When spec-ing something that calls method which takes a set of nested hashes (as many Rails methods do), it may be tempting to use #hash_including: to test for only the values you care about. However #hash_including won’t work the way we might hope for nested hashes. Take the following (highly contrived) example:
describe CoffeeMaker do
before :each do
@it = CoffeeMaker.new
end
it "should receive #make_coffee with roast => medium" do
@it.should_receive(:make_coffee).
with(:water => :filtered,
:beans => hash_including(:roast => :medium))
@it.make_coffee(:water => :filtered,
:beans => {
:origin => "Guatemala",
:roast => :medium
})
end
end
If we run this we get a failure:
1)
Spec::Mocks::MockExpectationError in 'CoffeeMaker should receive #make_coffee with roast => medium'
Mock 'CoffeeMaker' expected :make_coffee with ({:water=>:filtered, :beans=>#:dark}>}) but received it with ({:water=>:filtered, :beans=>{:roast=>:medium, :origin=>"Guatemala"}})
Clearly #hash_including was only intended to work with shallow hashes.
Instead, we can use a lesser-known feature of RSpec’s mock objects to test only the values we care about:
describe CoffeeMaker do
before :each do
@it = CoffeeMaker.new
end
it "should receive #make_coffee with roast => medium" do
@it.should_receive(:make_coffee) do |options|
options[:beans][:roast].should == :medium
end
@it.make_coffee(:water => :filtered,
:beans => {
:origin => "Guatemala",
:roast => :medium
})
end
end
Here we’ve supplied a block to #should_receive. The block will be called when the mocked method is called, and will be passed whatever arguments the mocked method was called with. Inside we can use any kind of RSpec assertions we like.
Here’s the failure message if we supply :roast => :dark instead of :medium:
Spec::Mocks::MockExpectationError in 'CoffeeMaker should receive #make_coffee with roast => medium' Mock 'CoffeeMaker' received :make_coffee but passed block failed with: expected: :medium, got: :dark (using ==)
Hmmm…This gives me a lot of good ideas…if they work out, I'll come back and share! Thanks again!
Used this at work today — thanks!
You are quite welcome!
Great post! How do you do this, if your method takes more than one argument? e.g. make_coffee(name, hash)
It’s just more arguments to the blog, e.g. “should_receive(:make_coffee) do |name, options| … end”
Thanks Avdi, saves the day 🙂