Complex Hash Expectations in RSpec

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 => {
                      :o rigin => "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, :o rigin=>"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 => {
                      :o rigin => "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 ==)

Related posts:

  1. Sustainable Development in Ruby, Part 2: Method Injection
  2. Sustainable Development in Ruby, Part 1: Good Old-Fashioned Inheritance
This entry was posted in Ruby, Uncategorized and tagged , , , . Bookmark the permalink.
  • sexykayleigh

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

  • Jraines

    Used this at work today — thanks!

    • http://avdi.org Avdi Grimm

      You are quite welcome!

  • http://twitter.com/morgler Matthias Orgler

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

    • http://avdi.org Avdi Grimm

      It’s just more arguments to the blog, e.g. “should_receive(:make_coffee) do |name, options| … end”