Avoid recreating the subject in RSpec - ruby-on-rails

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.

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 do we test interactor organizers using rspec?

I want to test below organizer interactor for, calling the 2 specified interactors without executing the calling interactors('SaveRecord, PushToService') code.
class Create
include Interactor::Organizer
organize SaveRecord, PushToService
end
I found few examples where the overall result of all the interactors logic(record should be saved and pushed to other service) has been tested. But, i dont want to execute the other interactor's logic as they will be tested as part of their separate specs.
1. Is it possible to do so?
2. Which way of testing(testing the overall result/testing only this particular
organizer interactor behavior) is a better practise?
I believe we need to test the interactor organizer for included interactors without executing the included interacors. I am able to find a way stub and test the organizer with below lines
To Stub:
allow(SaveRecord).to receive(:call!) { :success }
allow(PushToService).to receive(:call!) { :success }
To Test:
it { expect(interactor).to be_kind_of(Interactor::Organizer) }
it { expect(described_class.organized).to eq([SaveRecord, PushToService]) }
Found call! method & organized variable from interactor organizer source files where it is trying to call and use internally. Stubbing the call! method and testing the organized variable has fulfilled my requirement.
You can test they are called and the order:
it 'calls the interactors' do
expect(SaveRecord).to receive(:call!).ordered
expect(PushToService).to receive(:call!).ordered
described_class.call
end
See: https://relishapp.com/rspec/rspec-mocks/docs/setting-constraints/message-order
Just iterating over #prem answer.
To Test:
it { expect(interactor).to be_kind_of(Interactor::Organizer) }
it { expect(described_class.organized).to eq([SaveRecord, PushToService]) }
interactor in this case is an instance of the Interactor class, or in Rspec syntax:
let(:interactor) { described_class.new }

Rails / RSpec: 'allow_any_instance_of' doesn't return multiple values

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

rspec-given error `Then` is not available from within an example (e.g. an `it` block)

I'm using rspec-given and keep receiving this error.
Failure/Error: Then {
Then is not available from within an example (e.g. an it block) or from constructs that run in the scope of an example (e.g. before, let, etc). It is only available on an example group (e.g. a describe or context block).
describe SchoolService do
Given(:school) { create(:school_with_applications) }
Given(:service) { School.new(#school) }
describe 'create_default_programs_and_year_grades!' do
it 'checks program size' do
When { service.create_default_programs_and_year_grades! }
Then { expect(school.programs.size).to eq 3 }
end
end
end
The error message says it all:
Then is not available from within an example (e.g. an it block) or from constructs that run in the scope of an example (e.g. before, let, etc). It is only available on an example group (e.g. a describe or context block).
please read the error message carefully. And you have the solution in the error message itself.
You can't use Then inside a it block, you can only use Then either with describe or context block.
So, to solve your problem, just use context instead of it:
describe SchoolService do
Given(:school) { create(:school_with_applications) }
Given(:service) { School.new(#school) }
describe 'create_default_programs_and_year_grades!' do
context 'checks program size' do
When { service.create_default_programs_and_year_grades! }
Then { expect(school.programs.size).to eq 3 }
end
end
end
See more examples here.

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.

Resources