Reputation: 504
I've recently found this peculiar behavior in Ruby 2.0, Rails 4, and Rspec 2.13.1
When I stub an instance method using ClassName.any_instance.stub(:method_name)
, it correctly stubs and past instances I've created. However, when I restub it by changing the return value, the old instance returns the old stubbed value, not the newer stub value.
For instance, I have this dummy class definition
class A
def test(x)
return x
end
end
and this test describes the behavior:
it 'strange stubbing behavior' do
inst = A.new
inst.test(1).should eq 1 #passes
A.any_instance.stub(:test).and_return(10)
inst.test(0).should eq 10
A.any_instance.unstub(:test) #has no effect
A.any_instance.stub(:test).and_return(100)
inst.test(0).should eq 100 #expects 100, got 10
A.any_instance.stub(:test) do |a|
a + 2
end
inst.test(3).should eq 5 #also fails # also got 10
end
Why does rspec behave like this? Is it defined behavior? If so, then what is the proper way of restubbing old instances. Or is it a bug?
EDIT: Before anyone else gives a "questioning the question" answer, I'd like to point out that I DID solve my original problem by rethinking the specs and reorganizing them. However, I am still curious as to why RSpec behaves this way
Upvotes: 2
Views: 3362
Reputation: 29379
In short, it appears that RSpec indeed does not unstub an existing instance when you use any_instance.unstub
in the case where the stub has been invoked on that instance. Similarly, stubbing with any_instance.stub
will not work with previously stubbed/invoked instances, as you discovered.
However, if you don't invoke or explicitly unstub an existing instance, then any new stub
s with any_instance
will work as expected on that instance. For example, the following modification to your example will work.
it 'strange stubbing behavior' do
inst = A.new
inst.test(1).should eq 1 #passes
A.any_instance.stub(:test).and_return(10)
# SKIP THE INVOCATION SO TEST WILL PASS
# inst.test(0).should eq 10
A.any_instance.unstub(:test) #has no effect (on existing instances)
A.any_instance.stub(:test).and_return(100)
inst.test(0).should eq 100 #expects 100, got 10
inst.unstub(:test) # UNSTUB INSTANCE SO TEST WILL PASS
A.any_instance.stub(:test) do |a|
a + 2
end
inst.test(3).should eq 5 #also fails # also got 10
end
Not sure if this is the expected behavior, but submitted an issue for it. The RSpec Cucumber tests, viewable at https://www.relishapp.com/rspec/rspec-mocks/docs/method-stubs/stub-on-any-instance-of-a-class#any-instance-unstub, only test that unstub
works for new instances.
(Nod to @Theresa Luu for her help with this.)
Upvotes: 2
Reputation: 18075
In my opinion, the stub method isn't really what you want here.
Generally speaking, if I want to truly stub something, then it should really only need to return the 1 value I want it to. I have never found myself trying to "restub" something as you do above.
That said, there are ways to have your mocks return different values on different calls, which might be all you're trying to do here. In rspec, you can accomplish that by passing multiple values in to your mock call as parameters. For instance:
inst.stub(:test).and_return(1,2,3)
inst.test(0) #=> 1
inst.test(0) #=> 2
inst.test(0) #=> 3
Upvotes: 1