10

I'm trying to make sense of the tests in an inherited app, and I need some help.

There are lots of spec groups like this one (view spec):

let(:job_post) { FactoryGirl.create(:job_post) }

# ...

before do
  expect(view).to receive(:job_post).at_least(:once).and_return(job_post)
end

it "should render without error" do
  render
end

... with job_post being an helper method defined on the controller. (yes, they could have used @instance variables, and I'm in the process of refactoring it).

Now, in my opinion using an expect inside a before block is wrong. Let's forget about that for a second.

Normally the test above is green.
However, if I remove the expect line, the test fails. It appears that in this case expect is stubbing the method on the view. In fact, replacing expect with allow seems to have exactly the same effect.

I think that what's going on is that normally – when run with a server – the view will call job_posts and the message will land on the helper method on the controller, which is the expected behaviour.

Here, however, expect is setting an expectation and, at the same time, stubbing a method on the view with a fixed return value. Since the view template will call that method, the test passes.

About that unexpected "stub" side effect of expect, I've found this in the rspec-mocks readme:

(...) We can also set a message expectation so that the example fails if find is not called:

person = double("person")
expect(Person).to receive(:find) { person }

RSpec replaces the method we're stubbing or mocking with its own test-double-like method. At the end of the example, RSpec verifies any message expectations, and then restores the original methods.

Does anyone have any experience with this specific use of the method?

Dave Schweisguth
  • 36,475
  • 10
  • 98
  • 121
tompave
  • 11,952
  • 7
  • 37
  • 63

3 Answers3

25

Well, that's what expect().to receive() does! This is the (not so) new expectation syntax of rspec, which replaces the should_receive API

expect(view).to receive(:job_post).at_least(:once).and_return(job_post)

is equivalent to

view.should_receive(:job_post).at_least(:once).and_return(job_post)

and this API sets the expectation and the return value. This is the default behavior. To actually call the original method as well, you need to explicitly say so:

view.should_receive(:job_post).at_least(:once).and_call_original

On to some other issues:

(yes, they could have used @instance variables, and I'm in the process of refactoring it).

let API is very ubiquitous in rspec testing, and may be better than @instance variables in many cases (for example - it is lazy, so it runs only if needed, and it is memoized, so it runs at most once).

In fact, replacing expect with allow seems to have exactly the same effect.

The allow syntax replaces the stub method in the old rspec syntax, so yes, it has the same effect, but the difference is, that it won't fail the test if the stubbed method is not called.


As the OP requested - some explanations about should_receive - unit tests are expected to run in isolation. This means that everything which is not directly part of your test, should not be tested. This means that HTTP calls, IO reads, external services, other modules, etc. are not part of the test, and for the purpose of the test, you should assume that they work correctly.

What you should include in your tests is that those HTTP calls, IO reads, and external services are called correctly. To do that, you set message expectations - you expect the tested method to call a certain method (whose actual functionality is out of the scope of the test). So you expect the service to receive a method call, with the correct arguments, one or more times (you can explicitly expect how many times it should be called), and, in exchange for it actually being called, you stub it, and according to the test, set its return value.

Sources:

janniks
  • 2,942
  • 4
  • 23
  • 36
Uri Agassi
  • 36,848
  • 14
  • 76
  • 93
  • thanks. When I mentioned the instance variables I was actually talking about the controllers, not the tests. Who worked on the project before me had an aversion to views using controllers' @instance variables, and has created accessors for all of them. – tompave Mar 26 '14 at 20:29
  • Also, great answer... but could you please expand on `should_receive`? I've always used `stub` and `allow`, and I always assumed that `expect(obj).to receive` was setting an expectation __only__. – tompave Mar 26 '14 at 20:31
  • Thanks for the clarification. Yes, I'm aware of the importance of mocks and stubs in unit tests. However, I've always explicitly used `stub/allow` to stub a method, and then `shout_receive/expect().to receive` to test the message. I was just surprised that `expect` handles it automatically... and I find it a bit confusing, to be honest. – tompave Apr 03 '14 at 20:07
1

Rspec is a meta-gem, which depends on the rspec-core, rspec-expectations and rspec-mocks gems. Rspec-mocks is a test-double framework for rspec with support for method stubs, fakes, and message expectations on generated test-doubles and real objects alike.

allow().to receive

is the use of 'Method Stubs', however

expect().to receive

is the use of 'Message Expectations'

You can refer to the Doc for more details

hiveer
  • 660
  • 7
  • 17
1

If you don't want to stub as a side affect, you can always call the original.

https://relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/calling-the-original-method

For example I once wanted to spy on a method, but also call the function else it has other side affects. That really helped.

https://relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/calling-the-original-method

Pratik Bothra
  • 2,642
  • 2
  • 30
  • 44