RSpec's should_receive doesn't act quite the way you would guess!

For anyone more experienced with RSpec, this would be pretty obvious, but it took me some time to realize this was what was causing some crazy behavior that I just plain could not understand.

I'm writing RSpec tests for some code that lives in static methods. For illustration purposes, lets say it looks like this:


module Posterous
   class Foo

     def self.do_something
        do_something_delegate
     end

     def self.do_something_delegate
        puts "Hello world!"
        Posterous::Foo.complicated_stuff
    end

    def complicated_stuff
    ...
    end

  end
 end
 
So then I write a simple RSpec test that does this:

 it "should call do_something and its delegate" do 
   Posterous::Foo.should_receive(:do_something_delegate)
   Posterous::Foo.should_receive(:complicated_stuff)
   Posterous::Foo.do_something
 end
 
So what do you expect will happen? I expected that the test would pass -- in theory, nothing about the term should_receive screams to me that the method invocation would change at all.

That's just not how it works, however. Because should_receive is a part of the Spec::Mocks lib of RSpec, it actually DOES cause the method "do_something_delegate" to act like a mock. A mock model is an actual stand-in, and in this case, because we don't define a return value. We could return a mock value here if we called Posterous::Foo.should_receive(:do_something_delegate).and_return("my value here").

So do_something_delegate ends up becoming a no-op (and that passes), but then it fails on Posterous::Foo.should_receive(:complicated_stuff). This makes sense, since complicated_stuff was INSIDE of do_something_delegate, and since do_something_delegate is mocked and never actually called. Hence complicated_stuff is skipped entirely and that test fails.

And actually, that's what you want. Unit tests at this level should be simple, and should test only one layer of your call stack at any given point. Integration/controller tests can be used for end-to-end testing, but this kind of mock behavior is ideal because it forces you to only test that method call and the behavior contained within.

Sometimes the best lessons come from banging your head against a problem until you radically change the way you think.