Reputation: 11
I'm just finishing my first year writing production code in Rails, and I'm not really stuck here, but the non-rubyness of what I'm experiencing is weird, and I wanted to see if anyone had this problem.
Say you're writing code which involves an external API, so the calls you make to that API need to be carefully crafted. In order to make sure these calls are happening correctly, I want to write some specs which mandate that these calls only happen a certain a number of times and with certain arguments. Something like this:
it "only calls the wrapper once with the correct arguments" do
expect(Wrapper).to have_received(:do_magic).once
expect(Wrapper).to have_received(:do_magic).with(flowers_in_sleeve: false)
end
But having two expectations is bad practice! I use RuboCop to lint (and I like sticking to it's conventions) so there are two options for me: either break this into two expectations or disable the rule for having multiple expectations in a block.
So, this isn't a totally earth shattering problem. But it seems very strange to me that RSpec doesn't have a more convenient way to satisfy this case. Maybe I just need to suck it up and write the two expectations. My goal is to do this in one expectation if possible.
This was the main solution I thought of:
it "only calls the wrapper once with the correct arguments" do
expect(Wrapper).to have_received(:do_magic).once.with(flowers_in_sleeve: false)
end
But that allows many calls to the wrapper, so long as you call the method at least once with those arguments. Which, really, if you look at the sentence you wrote:
I expect wrapper to be called once with flowers in sleeve to be false
This behavior makes sense, because I'm not saying:
I expect wrapper to be called only once with flower in sleeve to be false
.
Upvotes: 1
Views: 200
Reputation: 26729
RuboCop wise you have couple of options.
Obvious one is to set the Max
for MultipleExpectation to 2. But that would apply to all tests.
A better one is to wrap the two expectations in an aggregate_failures
block.
As for the RSpec syntax, it's hard to specify whether you want this particular set of arguments to appear once, or whether it should have only one call and it should match the arguments.
That being said, you can do
expect(Wrapper).to receive(:do_magic).once do |flowers_in_sleeve:|
expect(flowers_in_sleeve).to eq(false)
end
which as far as I remember will also trigger the MultipleExpecations offence.
Upvotes: 0
Reputation: 698
As @max commented, having multiple expectations isn't necessarily bad practice. It's entirely possible that multiple expect
statements can be helpful to cover one behavior. You are free to modify the RuboCop defaults when you have a compelling reason to do so.
That being said, here's a pattern you can use to write specs that mandate that calls only happen a certain a number of times and with certain arguments. You can capture all the arguments passed to each call of do_magic
and then expect
against the set of arguments.
# lib/magic_performer.rb
class Wrapper
def self.do_magic(options = {})
# implementation goes here
end
end
class MagicPerformer
def perform_magic
Wrapper.do_magic(flowers_in_sleeve: false)
end
end
# spec/magic_performer_spec.rb
require 'rspec'
require_relative '../lib/magic_performer'
RSpec.describe MagicPerformer do
let(:args_in_calls_to_do_magic) { [] }
before do
allow(Wrapper).to receive(:do_magic) do |*args, **kwargs|
args_in_calls_to_do_magic << [args, kwargs]
end
MagicPerformer.new.perform_magic
end
it 'only calls the wrapper once with the correct arguments' do
expect(args_in_calls_to_do_magic)
.to eq([
[[], { flowers_in_sleeve: false }]
])
end
end
Separately, consider that this type of test is a "white-box" test (as opposed to a "black-box" test). Such a test is aware of the implementation details of MagicPerformer
, the class under test. Generally, it is better to write a test that is not aware of any such implementation details as MagicPerformer
internally calling Wrapper
. Black box tests enable you, in the future, to refactor the class under test—for example to remove its dependency on Wrapper
—without changing the tests. Consider whether it's possible to rewrite the test to expect only with respect to the observable behavior of Wrapper
; that is, to expect
against either the return value of MagicPerformer
or public state that it modifies when called.
Upvotes: 0