Stubbing corner case

Hi!

I was just talking to @dchelimsky over Twitter about a weird corner case
we’ve run into on 1.3.1 (I’ve also been able to reproduce it in 1.3.2)

So we’re using this gem called ClassyStruct that’s a higher performing
OpenStruct:

Basically it acts the same as OpenStruct but defines methods on the
object’s class instead of the object itself on the fly.
When it receives a call that hits method_missing it calls attr_accessor
on the method name then passes the call on to the object.

Our problem comes from having one spec that stubs out a call to the
object:
foo.stub! :bar => ‘test’

and later in another spec file trying to set the same method with a
value then having our code use that value:

spec

foo.bar = ‘other test’

code

puts “#{foo.bar} baz”

So our expectation is that foo.bar will return ‘other test’. Instead it
hits the old stub on foo which calls method_missing which is picked up
again by ClassyStruct causing it to fire off attr_accessor again then
passing the method through causing the stub to call method_missing and
on and on finally giving us a “stack level too deep” error.

The crux of the problem is that ClassyStruct is adding a method to the
class after Rspec has added the same method to the instance.

As I said it’s a very weird corner case because we’re calling
attr_accessor on a class that already has objects floating around. The
easiest way to fix this is to use stub! in both places.

Regardless we were surprised that the proxy sticks around after a test
run. What is the reason for keeping it around?

Thanks for your time!
Doug McInnes

On Apr 22, 2011, at 4:58 PM, Doug McInnes wrote:

Our problem comes from having one spec that stubs out a call to the object:
The crux of the problem is that ClassyStruct is adding a method to the class
after Rspec has added the same method to the instance.

As I said it’s a very weird corner case because we’re calling attr_accessor on a
class that already has objects floating around. The easiest way to fix this is to
use stub! in both places.

Regardless we were surprised that the proxy sticks around after a test run. What
is the reason for keeping it around?

There’s no intent to keep it around, so there is a bug at play here, but
let’s see if we can narrow it down.

Can you post (gist or pastie) an example that I can just run as/is to
see the behavior you’re seeing?

Sure!
Here’s the Gist:

It’s works in Rspec 2.5.1, but not in Rspec 1.3.2

Doug

On Apr 25, 2011, at 10:51 AM, Doug McInnes wrote:

On Apr 23, 2011, at 3:00 PM, David C. wrote:

Basically it acts the same as OpenStruct but defines methods on the object’s
class instead of the object itself on the fly.

Can you post (gist or pastie) an example that I can just run as/is to see the
behavior you’re seeing?

Any ideas on this issue?

Here’s the code:

class TestClass
end

our use case is with a stub on a constant

TEST = TestClass.new

describe TestClass do
it “works before the stub” do
TestClass.send(:attr_accessor, :foo)
TEST.foo = :baz

TEST.foo.should == :baz

TestClass.send(:remove_method, 'foo')
TestClass.send(:remove_method, 'foo=')

end

it “has a stubbed method” do
TEST.stub! :foo => :bar

TEST.foo.should == :bar

end

it “fails after the stub” do
TestClass.send(:attr_accessor, :foo)
TEST.foo = :baz

TEST.foo.should == :baz

end
end

And the console output:

~/tmp $ ruby -v
ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-darwin10.4.0]
~/tmp $ spec -v
rspec 1.3.2
~/tmp $ spec test.rb
…F

NoMethodError in ‘TestClass fails after the stub’
undefined method foo' for #<TestClass:0x00000101180158> test.rb:28:inblock (2 levels) in <top (required)>’

Finished in 0.046678 seconds

3 examples, 1 failure
~/tmp $ rspec -v
2.5.1
~/tmp $ rspec test.rb

Finished in 0.00154 seconds
3 examples, 0 failures

As I said this is a weird corner case :slight_smile:

Thanks,
Doug