RSpec allow/expect vs just expect/and_return

37,639

See the classic article Mocks Aren't Stubs. allow makes a stub while expect makes a mock. That is allow allows an object to return X instead of whatever it would return unstubbed, and expect is an allow plus an expectation of some state or event. When you write

allow(Foo).to receive(:bar).with(baz).and_return(foobar_result)

... you're telling the spec environment to modify Foo to return foobar_result when it receives :bar with baz. But when you write

expect(Foo).to receive(:bar).with(baz).and_return(foobar_result) 

... you're doing the same, plus telling the spec to fail unless Foo receives :bar with baz.

To see the difference, try both in examples where Foo does not receive :bar with baz.

Share:
37,639
Paul Fioravanti
Author by

Paul Fioravanti

Talks to computers.

Updated on July 09, 2022

Comments

  • Paul Fioravanti
    Paul Fioravanti almost 2 years

    In RSpec, specifically version >= 3, is there any difference between:

    • Using allow to set up message expectations with parameters that return test doubles, and then using expect to make an assertion on the returned test doubles
    • Just using expect to set up the expectation with parameters and return the test double

    or is it all just semantics? I know that providing/specifying a return value with expect was the syntax in RSpec mocks 2.13, but as far as I can see, the syntax changed in RSpec mocks 3 to use allow.

    However, in the (passing) sample code below, using either allow/expect or just expect/and_return seems to generate the same result. If one syntax was favoured over another, perhaps I would have expected there to be some kind of deprecation notice, but since there isn't, it would seem that both syntaxes are considered valid:

    class Foo
      def self.bar(baz)
        # not important what happens to baz parameter
        # only important that it is passed in
        new
      end
    
      def qux
        # perform some action
      end
    end
    
    class SomethingThatCallsFoo
      def some_long_process(baz)
        # do some processing
        Foo.bar(baz).qux
        # do other processing
      end
    end
    
    describe SomethingThatCallsFoo do
      let(:foo_caller) { SomethingThatCallsFoo.new }
    
      describe '#some_long_process' do
        let(:foobar_result) { double('foobar_result') }
        let(:baz) { double('baz') }
    
        context 'using allow/expect' do
          before do
            allow(Foo).to receive(:bar).with(baz).and_return(foobar_result)
          end
    
          it 'calls qux method on result of Foo.bar(baz)' do
            expect(foobar_result).to receive(:qux)
            foo_caller.some_long_process(baz)
          end
        end
    
        context 'using expect/and_return' do
          it 'calls qux method on result of Foo.bar(baz)' do
            expect(Foo).to receive(:bar).with(baz).and_return(foobar_result)
            expect(foobar_result).to receive(:qux)
            foo_caller.some_long_process(baz)
          end
        end
      end
    end
    

    If I deliberately make the tests fail by changing the passed-in baz parameter in the expectation to a different test double, the errors are pretty much the same:

      1) SomethingThatCallsFoo#some_long_process using allow/expect calls quux method on result of Foo.bar(baz)
         Failure/Error: Foo.bar(baz).qux
           <Foo (class)> received :bar with unexpected arguments
             expected: (#<RSpec::Mocks::Double:0x3fe97a0127fc @name="baz">)
                  got: (#<RSpec::Mocks::Double:0x3fe97998540c @name=nil>)
            Please stub a default value first if message might be received with other args as well.
         # ./foo_test.rb:16:in `some_long_process'
         # ./foo_test.rb:35:in `block (4 levels) in <top (required)>'
    
      2) SomethingThatCallsFoo#some_long_process using expect/and_return calls quux method on result of Foo.bar(baz)
         Failure/Error: Foo.bar(baz).qux
           <Foo (class)> received :bar with unexpected arguments
             expected: (#<RSpec::Mocks::Double:0x3fe979935fd8 @name="baz">)
                  got: (#<RSpec::Mocks::Double:0x3fe979cc5c0c @name=nil>)
         # ./foo_test.rb:16:in `some_long_process'
         # ./foo_test.rb:43:in `block (4 levels) in <top (required)>'
    

    So, are there any real differences between these two tests, either in result or expressed intent, or is it just semantics and/or personal preference? Should allow/expect be used over expect/and_return in general as it seems like it's the replacement syntax, or are each of them meant to be used in specific test scenarios?

    Update

    After reading Mori's answer's, I commented out the Foo.bar(baz).qux line from the example code above, and got the following errors:

      1) SomethingThatCallsFoo#some_long_process using allow/expect calls qux method on result of Foo.bar(baz)
         Failure/Error: expect(foobar_result).to receive(:qux)
           (Double "foobar_result").qux(any args)
               expected: 1 time with any arguments
               received: 0 times with any arguments
         # ./foo_test.rb:34:in `block (4 levels) in <top (required)>'
    
      2) SomethingThatCallsFoo#some_long_process using expect/and_return calls qux method on result of Foo.bar(baz)
         Failure/Error: expect(Foo).to receive(:bar).with(baz).and_return(foobar_result)
           (<Foo (class)>).bar(#<RSpec::Mocks::Double:0x3fc211944fa4 @name="baz">)
               expected: 1 time with arguments: (#<RSpec::Mocks::Double:0x3fc211944fa4 @name="baz">)
               received: 0 times
         # ./foo_test.rb:41:in `block (4 levels) in <top (required)>'
    
    • The allow spec fails because the foobar_result double never gets to stand in for the result of Foo.bar(baz), and hence never has #qux called on it
    • The expect spec fails at the point of Foo never receiving .bar(baz) so we don't even get to the point of interrogating the foobar_result double

    Makes sense: it's not just a syntax change, and that expect/and_return does have a purpose different to allow/expect. I really should have checked the most obvious place: the RSpec Mocks README, specifically the following sections: