applepie
applepie

Reputation: 504

rspec any_instance stub does not restub old instances

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

Answers (2)

Peter Alfvin
Peter Alfvin

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 stubs 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

Pete
Pete

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

Related Questions