Rspec mocks, can 'expect' also stub a method as a side effect? - ruby-on-rails

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?

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:
Message expectation
RSpec's New Expectation Syntax

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

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

Related

How to test if method is called in RSpec but do not override the return value

There are already questions similar to this, but they all override the return values to nil unless .and_return is called as well
PROBLEM
I am wondering if there is a way to just check if a method is called using expect_any_instance_of(Object).to receive(:somemethod) and it runs normally without overriding or affecting the return value of .somemethod.
rspec-3.4.0
rails 4.2
Consider the following:
# rspec
it 'gets associated user' do
expect_any_instance_of(Post).to receive(:get_associated_user)
Manager.run_processes
end
# manager.rb
class Manager
def self.run_processes
associated_user = Category.first.posts.first.get_associated_user
associated_user.destroy!
end
end
The spec above although will work because :get_associated_user is called in the run_processes, however it raises NoMethodError: undefined method 'destroy!' for NilClass precisely because I mocked the :get_associated_user for any instance of Post.
I could add a .and_return method like expect_any_instance_of(Post).to receive(:get_associated_user).and_return(User.first) so that it will work without raising that error, but that already is a mocked return value (which might affect the rest of the code below it), and not the correct expected value it should have returned at the time the method is called.
I can however specify .and_return(correct_user) where correct_user is the user that is going to be the same return value as if it has ran normally. However, this will need me to mock every return value in the sequence Category.first.posts.first.get_associated_user just so that it will work normally. The actual problem is a lot more complex than above, therefore stubbing is not really a possible solution in my case.
You can use and_call_original on the fluent interface to "pass
through" the received message to the original method.
https://www.relishapp.com/rspec/rspec-mocks/v/2-14/docs/message-expectations/calling-the-original-method
expect_any_instance_of(Post).to receive(:get_associated_user).and_call_original
However the use of expect_any_instance_of might be telling you that you have a code smell and you should be testing the behavior - not the implementation.
# test what it does - not how it does it.
it 'destroys the associated user' do
expect { Manager.run_processes }.to change(Category.first.posts.first.users, :count).by(-1)
end

Mocha on Ruby: Check a stubbed function called once

It should be straight forward, but it doesn't work for me.
I'm stubbing a function call, and I want to make sure it is called once, so I did:
MyClass.stubs(:record).returns(true).expect(:record).once
MyClass.run
but I keep getting:
expected exactly once, not yet invoked: allowed any number of times, invoked once: MyClass.record(any_parameters).record(any_parameters)
What am I doing wrong?
Are you trying to set expectations for 2 separate invocations on record?
stubs is just a syntactic sugar for expects, specifying that you expect an invocation zero or more times.
You could probably rewrite your example as such:
MyClass.expects(:record).returns(true)
Keep in mind that expects is by default implying the once part although you could add it if you think that it adds to your code's clarity.

In RSpec, what is the difference between a message expectation (receive) and a test spy (have_received)?

In RSpec (specifically rspec-mocks), what is the difference between Message Expectations and Test Spies? They seem similar, and appear right next to each other as seperate sections in the readme.
i.e. what is the difference between:
expect(validator).to receive(:validate) # message expectation
and
expect(validator).to have_received(:validate) # test spy
Message expectations can be set on any object and represent a declaration that something is going to happen (or not happen) to that object in the future. If the expectation is violated during subsequent test execution, the test will fail at the time the violation occurs. If the expectation has not been met by the end of the test, the test will fail as well.
The have_received family of methods only works on test doubles and examines what has happened to the double in the past, from the time of the double's creation up through the current method call. It succeeds or fails at that point in time. The term "test spy" is a little misleading, because the support for this backward-looking mechanism is a standard part of rspec-mocks at this point. You don't do anything "special" to create a "test spy".
You can't always use spies when you test, basically for expectations on classes.
Example:
expect(User).to receive(:new)
there is no way to do this with a spy (unless you do dependency injection).
Now, you could do the following:
user = double('user', save: true)
expect(User).to receive(:new).and_return user
User.new.save
expect(user).to have_received(:save)
You see clearly that:
you have to set expectations on real object before you run the real code (it kind of looks weird to set expectations before triggering the code)
you can set expectation on spies after the real code, which is more natural

Mocha Rails Weirdness

I'm seeing a very weird output from my Rails tests, using Mocha and Rails 3.1.0.
not all expectations were satisfied
unsatisfied expectations:
- expected exactly once, not yet invoked: #<GitAccess:0xbb5c344>.branches(any_parameters)
satisfied expectations:
- allowed any number of times, invoked once: #<GitAccess:0xbb5c344>.branches(any_parameters)
It says that my "branches" method was never called, but called once - on the same object? How is this possible? My controller looks like this:
def create
git_access.branches()
end
I'm totally not understanding how this is possible.
Okay, here's the answer. I somehow thought that .expects would only check whether the function is called or not. So in my test I had .expects and .stubs on the same function call, which made mocha ignore my .stubs.
By reading a bunch of tutorials online, .stubs should be used when you want to fake the response of a method, and .expects when you want to fake the response of a method AND test whether the method is called.

Set an expectation without mocking anything

Using MiniTest::Spec and Mocha:
describe "#devices" do
it "scopes the devices by the provided :ip_list" do
ips = 'fast tests ftw!'
ds = DeviceSearch.new ip_list: ips
Device.expects(:scope_by_ip_list).once.with(ips)
ds.devices
end
end
When I make the code work correctly, this test will fail, because calling Device.expects(:scope_by_ip_list) also stubs Device.scope_by_ip_list, and since I don't specify a .returns(Devices.scoped) or some such, it stubs out the method with nil. So, in my code which properly scopes a list of devices and then does further operations, the further operations blow up.
I don't want to have to specify a .returns parameter, though, because I totally don't care what it returns. I don't want to stub the method at all! I just want to set up an expectation on it, and leave it functioning just the way it is.
Is there a way to do that?
(To me, it seems very awkward to say something like Device.expects(:foo).returns('bar')—when I say that Model expects method, I'm not saying to stub that method! We can say Device.stubs(:foo), if we want to stub it.)
The behavior is intended and can't be changed. Look at the following post to see how it can be circumwented:
rspec 2: detect call to method but still have it perform its function

Resources