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 🙂