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 ==)

Published by Avdi Grimm

6 Comments

  1. Hmmm…This gives me a lot of good ideas…if they work out, I'll come back and share! Thanks again!

    Reply
  2. Used this at work today — thanks!

    Reply
  3. Great post! How do you do this, if your method takes more than one argument? e.g. make_coffee(name, hash)

    Reply
  4. Thanks Avdi, saves the day 🙂

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *