Rails / RSpec: 'allow_any_instance_of' doesn't return multiple values - ruby-on-rails

I have this code in one of my tests:
it 'returns ids when successful' do
allow_any_instance_of(Importer).to receive(:import).and_return('12589', '12590', '12591', '12592', '12593', '12594')
expect(#dispatcher.run).to eq(['12589', '12590', '12591', '12592', '12593', '12594'])
end
The test fails because it only return the first value:
expected: ["12589", "12590", "12591", "12592", "12593", "12594"]
got: ["12589", "12589", "12589", "12589", "12589", "12589"]
I just saw that #and_return's capability of returning multiple values only works when used with #allow.
What can I do for #allow_any_instance_of to get this behaviour?
EDIT:
The class I am testing is called Dispatcher. It takes an xml file, and splits it into parts concerning exactly one object. Each of those splitted parts is taken by the Importer which returns exactly one ID. The Dispatcher then creates an Array from those Ids. So, no, I am not expecting an array to be returned by the Importer.

The class I am testing, Dispatcher, calls Importer for every file it finds in an input directory.
Here's what should work (intercept importer creation)
class Dispatcher
def run
files.each do |file|
create_importer(file).import
end
end
def create_importer(file)
::Importer.new(file)
end
end
# spec
let(:fake_importer) { ::Importer.new }
before do
allow(#dispatcher).to receive(:create_importer).and_return(fake_importer)
allow(fake_importer).to receive(:import).and_return(your, multiple, values, here)
end

Related

Trouble mocking `Resolv::DNS.open`

I'm trying to mock the code below using MiniTest/Mocks. But I keep getting this error when running my test.
Minitest::Assertion: unexpected invocation: #<Mock:0x7fa76b53d5d0>.size()
unsatisfied expectations:
- expected exactly once, not yet invoked: #<Mock:0x7fa76b53d5d0>.getresources("_F5DC2A7B3840CF8DD20E021B6C4E5FE0.corwin.co", Resolv::DNS::Resource::IN::CNAME)
satisfied expectations:
- expected exactly once, invoked once: Resolv::DNS.open(any_parameters)
code being tested
txt = Resolv::DNS.open do |dns|
records = dns.getresources(options[:cname_origin], Resolv::DNS::Resource::IN::CNAME)
end
binding.pry
return (txt.size > 0) ? (options[:cname_destination].downcase == txt.last.name.to_s.downcase) : false
my test
::Resolv::DNS.expects(:open).returns(dns = mock)
dns.expects(:getresources)
.with(subject.cname_origin(true), Resolv::DNS::Resource::IN::CNAME)
.returns([Resolv::DNS::Resource::IN::CNAME.new(subject.cname_destination)])
.once
Right now you are testing that Resolv::DNS receives open returns your mock but
since you seem to be trying to test that the dns mock is receiving messages you need to stub the method and provide it with the object to be yielded
Try this instead:
dns = mock
dns.expects(:getresources)
.with(subject.cname_origin(true), Resolv::DNS::Resource::IN::CNAME)
.once
::Resolv::DNS.stub :open, [Resolv::DNS::Resource::IN::CNAME.new(subject.cname_destination)], dns do
# whatever code actually calls the "code being tested"
end
dns.verify
The second argument to stub is the stubbed return value and third argument to stub is what will be yielded to the block in place of the original yielded.
In RSpec the syntax is a bit simpler (and more semantic) such that:
dns = double
allow(::Resolv::DNS).to receive(:open).and_yield(dns)
expect(:dns).to receive(:getresources).once
.with(subject.cname_origin(true), Resolv::DNS::Resource::IN::CNAME)
.and_return([Resolv::DNS::Resource::IN::CNAME.new(subject.cname_destination)])
# whatever code actually calls the "code being tested"
You can write more readable integration tests with DnsMock instead of stubbing/mocking parts of your code: https://github.com/mocktools/ruby-dns-mock

How to use meck in different ExUnit test file

I would like to use meck in different ExUnit test file.
For example,
[x_test.exs]
def setup do
:meck.new(Hoge, [:passthrough])
 on_exit(fn -> :meck.unload end)
 :ok
end
def teardown do
:meck.unload
end
test "foo" do
:meck.expect(Hoge, :foo, fn -> 1 end)
assert Hoge.foo == 1
end
[y_test.exs]
def setup do
:meck.new(Hoge, [:passthrough])
 on_exit(fn -> :meck.unload end)
 :ok
end
def teardown do
:meck.unload
end
test "foo" do
:meck.expect(Hoge, :foo, fn -> 2 end)
assert Hoge.foo == 2
end
Sometimes, x_test.exs is fail, but sometimes, x_test.exs is success...
(y_test.exs is same)
Can I use mock to same function in another test file?
meck currently compiles and loads code you specified with your expectations. As only one current version of the code could be loaded inside the beam you should execute all tests that race for the same mocked function sequentially.
As ExUnit documentation states that test cases are executed in parallel, you probably have to merge all the tests that should be executed serially in the single test case (i.e. single test module).
Alternatively, you could set number of test cases that could be executed in parallel to 1. However it could slow down your test run
ExUnit supports the following options:
:max_cases - maximum number of cases to run in parallel; defaults to :erlang.system_info(:schedulers_online)

How to test log content with RSpec?

For example, it generated a log(my_work.log) content as:
I, [2015-05-14T00:00:00.000000 #5590] INFO -- : Work started.
I want to test if my_work.log has content Work started., how to do?
I don't want to match all line include datetime, because that contains #5590, I can't stub it.
You can pass in an instance of StringIO when initializing Logger to capture the output and then match on the expected content:
require 'logger'
describe "log" do
let(:log) { StringIO.new }
let(:logger) { Logger.new(log) }
let(:expected) { "Work started" }
it "should add the expected content to the log" do
logger.info(expected)
log.rewind
expect(log.read).to match(/.+#{expected}$/)
end
end
Rails.logger uses some methods to log things, for example:
debug
info
fatal
warn
So in your case you use info, to log something, instead loking for a match, you can detect if method info was called:
it 'logs exception' do
# some code
expect(Rails.logger).to receive(:info)
# execute the line that logs something
end
Even you can add parameters to receive method with reserved word with:
expect(Rails.logger).to receive(:info).with('Work started.')
This cause you need to specify something
Check rspec and rails logger
Also check this stackoverflow post
With RSpec's output matcher (introduced in 3.0) you can do the following:
expect { my_method }.to output("my message").to_stdout
expect { my_method }.to output("my error").to_stderr
In case of libraries such as Logger or Logging you may have to use output.to_<stdout/stderr>_from_any_process.
It's simple, clean and will test whether your messages actually reach the output.

rspec-mocks' doubles are designed to only last for one example

I've got a question about how to share rspec-mocks' double between examples. I'm writing a new rails app with rspec-mocks 3.1.3. I'm used to using the old (< 2.14 and and trying to update my knowledge if current rspec usage.
I have a model method:
def self.from_strava(activity_id, race_id, user)
#client ||= Strava::Api::V3::Client.new(access_token: 'abc123')
activity = #client.retrieve_an_activity(activity_id)
result_details = {race_id: race_id, user: user}
result_details[:duration] = activity['moving_time']
result_details[:date] = Date.parse(activity['start_date'])
result_details[:comment] = activity['description']
result_details[:strava_url] = "http://www.strava.com/activities/#{activity_id}"
Result.create!(result_details)
end
And here is the spec:
describe ".from_strava" do
let(:user) { FactoryGirl.build(:user) }
let(:client) { double(:client) }
let(:json_response) { JSON.parse(File.read('spec/support/strava_response.json')) }
before(:each) do
allow(Strava::Api::V3::Client).to receive(:new) { client }
allow(client).to receive(:retrieve_an_activity) { json_response }
allow(Result).to receive(:create!)
end
it "sets the duration" do
expect(Result).to receive(:create!).with(hash_including(duration: 3635))
Result.from_strava('123', 456, user)
end
it "sets the date" do
expect(Result).to receive(:create!).with(hash_including(date: Date.parse("2014-11-14")))
Result.from_strava('123', 456, user)
end
end
When I run a single test on it's own it's fine, but when I run the whole describe ".from_strava" block it fails with the message
Double :client was originally created in one example but has leaked into another example and can no longer be used. rspec-mocks' doubles are designed to only last for one example, and you need to create a new one in each example you wish to use it for.
I understand what it's saying, but surely this is an appropriate use of a double being used in 2 examples. After all, the client double isn't important to the example, it's just a way for me to load the canned response. I guess I could use WebMock but that seems very low-level and doesn't translate well to the actual code written. We should only be asserting one thing per example after all.
I had thought about replacing the client double with a call to
allow(Strava::Api::V3::Client).to receive_message_chain(:new, :retrieve_an_activity) { json_response }
but that doesn't seem to be the right approach either, given that the documentation states that receive_message_chain should be a code smell.
So if I shouldn't use receive_message_chain, shared client double and also follow the standard DRY principle then how should I fix this?
I would love some feedback on this.
Thanks,
Dave
Caching clients for external components can often be really desired (keeping alive connections/any SSL setup that you might need, etc.) and removing that for the sake of fixing an issue with tests is not a desirable solution.
In order to fix your test (without refactoring your code), you can do the following to clear the instance variable after each of your tests:
after { Result.instance_variable_set("#client", nil) }
While admittedly, this is not the cleanest solution, it seems to be the simplest and achieves both, lets you have a clear setup with no state shared in between tests, and keep your client cached in "normal" operation mode.
surely this is an appropriate use of a double being used in 2 examples.
No, it's not. :) You're trying to use a class variable; do not do that because the variable doesn't span examples. The solution is to set the client each time i.e. in each example.
Bad:
#client ||= Strava::Api::V3::Client.new(access_token: 'abc123')
Good:
#client = Strava::Api::V3::Client.new(access_token: 'abc123')
I had the same use case in an app of mine, and we solved it by extracting the cacheing into a private method and then stubbing that method to return the double (instead of stubbing the new method directly).
For example, in the class under test:
def self.from_strava(activity_id, race_id, user)
activity = strava_client.retrieve_an_activity(activity_id)
...
end
private
def self.strava_client
#client ||= Strava::Api::V3::Client.new(access_token: 'abc123')
end
And in the spec:
let(:client) { double(:client) }
before { allow(described_class).to receive(:strava_client).and_return(client) }
...
TLDR: Add after { order.vendor_service = nil } to balance the before block. Or read on...
I ran into this, and it was not obvious where it was coming from. In order_spec.rb model tests, I had this:
describe 'order history' do
before do
service = double('VendorAPI')
allow(service).to receive(:order_count).and_return(5)
order.vendor_service = service
end
# tests here ..
end
And in my Order model:
def too_many_orders?
##vendor_service ||= VendorAPI.new(key: 'abc', account: '123')
return ##vendor_service.order_count > 10
end
This worked fine when I only ran rspec on order_spec.rb
I was mocking something completely different in order_controller_spec.rb a little differently, using allow_any_instance_of() instead of double and allow:
allow_any_instance_of(Order).to receive(:too_many_orders?).and_return(true)
This, too, tested out fine.
The confounding trouble is that when I ran the full suite of tests, I got the OP's error on the controller mock -- the one using allow_any_instance. This was very hard to track down, as the problem (or at least my solution) lay in the model tests where I use double/allow.
To fix this, I added an after block clearing the class variable ##vendor_service, balancing the before block's action:
describe 'order history' do
before do
service = double('VendorAPI')
allow(service).to receive(:order_count).and_return(5)
order.vendor_service = service
end
after do
order.vendor_service = nil
end
# tests here ..
end
This forced the ||= VendorAPI.new() to use the real new function in later unrelated tests, not the mock object.

Avoid recreating the subject in RSpec

Say I have the following test
describe "bob" do
subject {
response = get "/expensive_lookup"
JSON.parse(response.body)
}
its(["transaction_id"]) { should == 1 }
its(["order_id"]) { should == 33 }
end
Then for each its() {} the subject will be reevaluated, which in my case it is a very slow lookup.
I could bundle all my tests together in one like
describe "bob" do
subject(res) {
response = get "/expensive_lookup"
JSON.parse(response.body)
}
it "returns the right stuff" do
res["transaction_id"]).should == 1
res["order_id"].should == 33
end
end
But this makes it less obvious which line of the test has failed if there is a failure.
Is there a way I can stop the subject from being reevaluated for each it block?
You can put that in to a before(:all) block. I don't know if that syntax has changed in a new rspec version, but regardless, your test would become this:
before(:all) do
response = get "/expensive_lookup"
#res = JSON.parse(response.body)
end
it "returns the right transaction ID" do
#res["transaction_id"].should == 1
end
# etc
The pro is that the code in the before-all block gets run just once for your spec. The con is that, as you can see, you can't take advantage of the subject; you need to write each more explicitly. Another gotcha is that any data saved to the test database is not part of the transaction and will not be rolled back.
There are two possible source of issues
Network request is slow/prone to fail
You should really mock all you network requests, slow or not.
The gem VCR is excellent. It makes it trivial to run your request once and persist the result for subsequent testing.
Building the immutable subject is slow
If you have multiple it blocks, the subject will be rebuild every time. Assuming you don't modify the subject, you can build it once.
You can use before(:all):
before(:all) { #cache = very_long_computation.freeze }
subject { #cache }
Note: that I call freeze to avoid modifying it by mistake, but of course that's not a deep freeze so you still need to mind what you are doing. If you are mutating your subject, your tests are no longer independent and shouldn't share the subject.

Resources