Message Expectations on methods that receive multiple messages with RSpec

Suppose you have the following scenario:

class Foo
  def bar
    [:foo,:bar,:zoo].each { |var| instance_variable_set("@#{var.to_s}", rand(5)) }
  end
end

And you want to specify that Foo should receive instance_variable_set with :foo, :bar and :zoo. You can try this way:

describe Foo
  describe "#bar"
    subject { Foo.new }
    [...]
    it "sets the instance variable @zoo with a random number" do
      subject.should_receive(:instance_variable_set).with(:zoo)
      subject.bar
    end
  end
end

But this way when #bar calls instance_variable_set with :foo or :bar, you get a Spec::Mocks::MockExpectationError: “#<Foo:0x7f770e216d08> received :instance_variable_set with unexpected arguments”. Great, ain’t it? How are we supposed to fix that?

Here’s something that should work in this case:

describe Foo
  describe "#bar"
    subject { Foo.new }
    [...]
    it "sets the instance variable @zoo with a random number" do
      subject.stub(:instance_variable_set).as_null_object
      subject.should_receive(:instance_variable_set).with(:zoo)
      subject.bar
    end
  end
end

Since you don’t care, in this example, what instance_variable_set does, you can stub it as a null_object, which won’t complain when it gets unexpected messages. So instead of the #bar method actually calling instance_variable_set, it calls the stub!

Do you still want to assert that instance_variable_set is doing its job on setting variables? Then it’s a behavior that should be specified on another example! An example would be the following:

describe Foo
  describe "#bar"
    subject { Foo.new }
    [...]
    it "sets the instance variable @zoo with a random number" do
      subject.stub(:rand => (fake_random = double('fake-random')))
      subject.bar
      subject.instance_variable_get("@zoo").should == fake_random
    end
  end
end 

This approach is fairly simple and works as it should, letting you specify your code behavior without bothering with the need to assert that “ruby is doing its job right”.

What about you? Do you have any magic you would like to share about setting message expectations? Feel invited to comment!

  • http://twitter.com/thiagopradi Thiago Pradi

    Howdy Son!

    My 5 cents: I use mocks/stubs only when I need access to external services (HTTP Requests, API’s). Otherwise, I always avoid mocking. overmocking is bad and generate brittle tests. This blog post has a nice point about mocking: http://patmaddox.com/blog/2009/3/9/you-probably-dont-get-mocks.html

    Also, your specs should always test what the method should do, not HOW it should do.

    Thiago

    • http://rubynoobie.wordpress.com/ Lucas d’Acampora Prim

      I actually use this approach when i need to specify behavior between objects or methods!
      To me, mocking is great as it allows you to focus on the behavior you are specifying. In the end, i guess it’s all a matter of taste and what works better for you.
      Great article btw! :)

    • http://mwilden.blogspot.com Mark Wilden

      Actually, the issue isn’t quite so cut and dried. There are two quite different styles to testing. You could call them “white box” (which uses mocks to verify behavior) and “black box” which uses public interfaces to verify state). Each have their adherents and I use both on occasion.

      See Martin Fowler’s explanation of the differences at http://martinfowler.com/articles/mocksArentStubs.html.

      • http://techie.lucaspr.im Lucas d’Acampora Prim

        Really loved Fowler’s article! It gave me a really better understanding of what we are talking about here! Thanks a lot!

  • Cort3z

    What do you do when your test need to verify some sub-property of the variable sent to the method? I currently use this:

    expect(subject).to receive(:the_method) { |object| expect(object.some_property).to eq(“Some String”) }

    testing_object.do_work

    The testing_object.do_work executes several “the_method”-calls on the subject, and I would like to verify that the object.some_property is set correctly.

    The method you describe above with stubbing does not appear to work in this case.