RSpec allow/expect vs just expect/and_return
Asked Answered
C

1

71

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:

Catholic answered 18/1, 2015 at 3:41 Comment(0)
S
174

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.

Superb answered 18/1, 2015 at 4:1 Comment(2)
One incidental advantage of 'expect' over 'allow' - aside from implementation details - is that if an 'allow' becomes irrelevant to your test, it becomes dead code that the computer won't warn you about. Dynamic languages have an advantage, that it's trivial to wrap them with a universal delegator object that will explode on destruction if the delegator was never used to forward messages.Mctyre
This is an incorrect assumption - expect is not a mock, nor a stub, it is an expectation, which could be set to wait both for a stubbed, or a real method to be called. The RSpec documentation is not the clearest one, so to clarify: A stub is the allow(obj).to receive(:method). A mock is allow(obj).to receive(:method).and_return(something).Systematics

© 2022 - 2024 — McMap. All rights reserved.