Issues with RSpec expect and method_missing

Hi ruby-talk,

I have an issue and unfortunately I believe my lack of knowledge about
how RSpec's expect functionality works and I'm having trouble working
around it.

I'm creating a class where an instance of that class will create a
singleton method dynamically if the request matches some criteria. The
feature itself seems to work unless I try to test it using RSpec, and
in that case it fails.

For example, given this class in file a.rb:

a_spec.rb (382 Bytes)

a.rb (234 Bytes)

···

---
class A

   private
   def method_missing(symbol, *args)
     if symbol.to_s.match(/dynamic/)
       puts 'creating it'
       define_singleton_method(symbol) { puts "created #{symbol}" }
     else
       super
     end
   end

end
---

and this test in a_spec.rb:

---
require_relative 'a'

describe A do
  it 'creates a dynamic method' do
    expect(subject.respond_to? :dynamic_method).to eq false
    subject.dynamic_method
    expect(subject.respond_to? :dynamic_method).to eq true
  end

  it 'only creates the dynamic method once' do
    # expect(subject).to receive(:define_singleton_method).once
    2.times { subject.dynamic_method }
  end
end
---

Everything is fine. I only see the method creation happen once:

rspec a_spec.rb -cfd

A
creating it
  creates a dynamic method
creating it
created dynamic_method
  only creates the dynamic method once

Finished in 0.00119 seconds (files took 0.11066 seconds to load)
2 examples, 0 failures

However as soon as I try to test it by uncommenting the expect call in
the second test, the test fails because now we are receiving the
define_singleton method for every call:

rspec a_spec.rb -cfd

A
creating it
  creates a dynamic method
creating it
creating it
  only creates the dynamic method once (FAILED - 1)

Failures:

  1) A only creates the dynamic method once
     Failure/Error: 2.times { subject.dynamic_method }
       (#<A:0x007fd0c48a8e40>).define_singleton_method(:dynamic_method)
           expected: 1 time with any arguments
           received: 2 times with arguments: (:dynamic_method)
     # ./a.rb:7:in `method_missing'
     # ./a_spec.rb:12:in `block (3 levels) in <top (required)>'
     # ./a_spec.rb:12:in `times'
     # ./a_spec.rb:12:in `block (2 levels) in <top (required)>'

Finished in 0.01253 seconds (files took 0.114 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./a_spec.rb:10 # A only creates the dynamic method once

Something about testing the functionality is altering it's behavior.
Anyone know how I can deal with this? If I'm not testing it correctly,
is there a better way?

I have attached the files I'm using.

Thanks,

Raj

Ignoring all of the rspec/testing aspects of this... Every time you hit your method_missing and generate a new singleton method on an instance you're blowing out your method cache. It pretty much does the opposite of what you want to do. I highly recommend that you take a less clever / dynamic approach and declare your methods ahead of time.

···

On Apr 14, 2015, at 12:48, Raj Sahae <rajsahae@gmail.com> wrote:

class A

   private
   def method_missing(symbol, *args)
     if symbol.to_s.match(/dynamic/)
       puts 'creating it'
       define_singleton_method(symbol) { puts "created #{symbol}" }
     else
       super
     end
   end

end

Maybe expects() calls another method that method_missing is catching?
(try logging 'symbol' with "creating it")

otherwise, you may want to step through the code with a debugger to see
what's happening?

···

On Tue, 2015-04-14 at 19:48 +0000, Raj Sahae wrote:

Hi ruby-talk,
I have an issue and unfortunately I believe my lack of knowledge about how RSpec's expect functionality works and I'm having trouble working around it.

I'm creating a class where an instance of that class will create a singleton method dynamically if the request matches some criteria. The feature itself seems to work unless I try to test it using RSpec, and in that case it fails.

For example, given this class in file a.rb:

---
class A

   private
   def method_missing(symbol, *args)
     if symbol.to_s.match(/dynamic/)
       puts 'creating it'
       define_singleton_method(symbol) { puts "created #{symbol}" }
     else
       super
     end
   end

end
---

and this test in a_spec.rb:

---
require_relative 'a'

describe A do
  it 'creates a dynamic method' do
    expect(subject.respond_to? :dynamic_method).to eq false
    subject.dynamic_method
    expect(subject.respond_to? :dynamic_method).to eq true
  end

  it 'only creates the dynamic method once' do
    # expect(subject).to receive(:define_singleton_method).once
    2.times { subject.dynamic_method }
  end
end
---

Everything is fine. I only see the method creation happen once:

> rspec a_spec.rb -cfd

A
creating it
  creates a dynamic method
creating it
created dynamic_method
  only creates the dynamic method once

Finished in 0.00119 seconds (files took 0.11066 seconds to load)
2 examples, 0 failures

However as soon as I try to test it by uncommenting the expect call in the second test, the test fails because now we are receiving the define_singleton method for every call:

> rspec a_spec.rb -cfd

A
creating it
  creates a dynamic method
creating it
creating it
  only creates the dynamic method once (FAILED - 1)

Failures:

  1) A only creates the dynamic method once
     Failure/Error: 2.times { subject.dynamic_method }
       (#<A:0x007fd0c48a8e40>).define_singleton_method(:dynamic_method)
           expected: 1 time with any arguments
           received: 2 times with arguments: (:dynamic_method)
     # ./a.rb:7:in `method_missing'
     # ./a_spec.rb:12:in `block (3 levels) in <top (required)>'
     # ./a_spec.rb:12:in `times'
     # ./a_spec.rb:12:in `block (2 levels) in <top (required)>'

Finished in 0.01253 seconds (files took 0.114 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./a_spec.rb:10 # A only creates the dynamic method once

Something about testing the functionality is altering it's behavior. Anyone know how I can deal with this? If I'm not testing it correctly, is there a better way?

I have attached the files I'm using.

Thanks,

Raj

Hi Ryan,

That is definitely a problem I didn't consider (and wish I had). However,
the nature of this application makes creating all the methods in a regular
sort of way an inordinate amount of work, and very un-maintainable. Any
suggestions on a better way to do this?

The use case here is that each sub object is associated with a string path
'/some/object/path'. Querying a service with this path produces some data
that includes information about subpaths. These subpaths may change based
on the external system during runtime so I'm attempting to account for that
with dynamic methods.

Thanks,
Raj

···

On Tue, Apr 14, 2015 at 3:44 PM Ryan Davis <ryand-ruby@zenspider.com> wrote:

> On Apr 14, 2015, at 12:48, Raj Sahae <rajsahae@gmail.com> wrote:
>
> class A
>
> private
> def method_missing(symbol, *args)
> if symbol.to_s.match(/dynamic/)
> puts 'creating it'
> define_singleton_method(symbol) { puts "created #{symbol}" }
> else
> super
> end
> end
>
> end

Ignoring all of the rspec/testing aspects of this... Every time you hit
your method_missing and generate a new singleton method on an instance
you're blowing out your method cache. It pretty much does the opposite of
what you want to do. I highly recommend that you take a less clever /
dynamic approach and declare your methods ahead of time.

It's creating dynamic_method twice

rspec a_spec.rb -cfd

A

creating it: dynamic_method

  creates a dynamic method

creating it: dynamic_method

creating it: dynamic_method

  only creates the dynamic method once (FAILED - 1)

My "guess" as to what might be happening is that the expect method is
wrapping subject up in a proxy and somehow that proxy is causing the logic
for define_singleton_method to work incorrectly. Maybe the singleton is
getting created in the proxy's singleton class and not the singleton class
of the actual object under test?

···

On Tue, Apr 14, 2015 at 1:05 PM Recursive Madman <recursive.madman@gmx.de> wrote:

Maybe expects() calls another method that method_missing is catching?
(try logging 'symbol' with "creating it")

otherwise, you may want to step through the code with a debugger to see
what's happening?

On Tue, 2015-04-14 at 19:48 +0000, Raj Sahae wrote:
> Hi ruby-talk,
> I have an issue and unfortunately I believe my lack of knowledge about
how RSpec's expect functionality works and I'm having trouble working
around it.
>
> I'm creating a class where an instance of that class will create a
singleton method dynamically if the request matches some criteria. The
feature itself seems to work unless I try to test it using RSpec, and in
that case it fails.
>
> For example, given this class in file a.rb:
>
> ---
> class A
>
> private
> def method_missing(symbol, *args)
> if symbol.to_s.match(/dynamic/)
> puts 'creating it'
> define_singleton_method(symbol) { puts "created #{symbol}" }
> else
> super
> end
> end
>
> end
> ---
>
> and this test in a_spec.rb:
>
> ---
> require_relative 'a'
>
> describe A do
> it 'creates a dynamic method' do
> expect(subject.respond_to? :dynamic_method).to eq false
> subject.dynamic_method
> expect(subject.respond_to? :dynamic_method).to eq true
> end
>
> it 'only creates the dynamic method once' do
> # expect(subject).to receive(:define_singleton_method).once
> 2.times { subject.dynamic_method }
> end
> end
> ---
>
> Everything is fine. I only see the method creation happen once:
>
> > rspec a_spec.rb -cfd
>
> A
> creating it
> creates a dynamic method
> creating it
> created dynamic_method
> only creates the dynamic method once
>
> Finished in 0.00119 seconds (files took 0.11066 seconds to load)
> 2 examples, 0 failures
>
> However as soon as I try to test it by uncommenting the expect call in
the second test, the test fails because now we are receiving the
define_singleton method for every call:
>
> > rspec a_spec.rb -cfd
>
> A
> creating it
> creates a dynamic method
> creating it
> creating it
> only creates the dynamic method once (FAILED - 1)
>
> Failures:
>
> 1) A only creates the dynamic method once
> Failure/Error: 2.times { subject.dynamic_method }
> (#<A:0x007fd0c48a8e40>).define_singleton_method(:dynamic_method)
> expected: 1 time with any arguments
> received: 2 times with arguments: (:dynamic_method)
> # ./a.rb:7:in `method_missing'
> # ./a_spec.rb:12:in `block (3 levels) in <top (required)>'
> # ./a_spec.rb:12:in `times'
> # ./a_spec.rb:12:in `block (2 levels) in <top (required)>'
>
> Finished in 0.01253 seconds (files took 0.114 seconds to load)
> 2 examples, 1 failure
>
> Failed examples:
>
> rspec ./a_spec.rb:10 # A only creates the dynamic method once
>
> Something about testing the functionality is altering it's behavior.
Anyone know how I can deal with this? If I'm not testing it correctly, is
there a better way?
>
> I have attached the files I'm using.
>
> Thanks,
>
> Raj

Hi Raj Sahae,

In order to test how many times a method was called, RSpec defines its
own singleton_method version of it. I think this could be causing your
problem.

require_relative 'a'

describe A do
  it 'creates a dynamic method' do
    expect(subject.respond_to? :dynamic_method).to eq false
    subject.dynamic_method
    expect(subject.respond_to? :dynamic_method).to eq true
  end

  it 'only creates the dynamic method once' do

    # CHECK here how the expectation changes the singleton methods list
    STDOUT.puts subject.singleton_methods.inspect
    expect(subject).to receive(:define_singleton_method).once
    STDOUT.puts subject.singleton_methods.inspect
    STDOUT.puts subject.method(:define_singleton_method).source_location.inspect

    2.times { subject.dynamic_method }
  end
end

The output here is

# => An empty array before the expectation definition

# A "fake" define_singleton_method after the expectation definition.
# The "fake" one comes from RSpec itself.

[:define_singleton_method]
["/home/abinoam/.rvm/gems/ruby-2.1.5/gems/rspec-mocks-3.2.1/lib/rspec/mocks/method_double.rb",
59]

Best regards,
Abinoam Jr.

···

On Tue, Apr 14, 2015 at 9:18 PM, Raj Sahae <rajsahae@gmail.com> wrote:

Hi Ryan,

That is definitely a problem I didn't consider (and wish I had). However,
the nature of this application makes creating all the methods in a regular
sort of way an inordinate amount of work, and very un-maintainable. Any
suggestions on a better way to do this?

The use case here is that each sub object is associated with a string path
'/some/object/path'. Querying a service with this path produces some data
that includes information about subpaths. These subpaths may change based on
the external system during runtime so I'm attempting to account for that
with dynamic methods.

Thanks,
Raj

On Tue, Apr 14, 2015 at 3:44 PM Ryan Davis <ryand-ruby@zenspider.com> wrote:

> On Apr 14, 2015, at 12:48, Raj Sahae <rajsahae@gmail.com> wrote:
>
> class A
>
> private
> def method_missing(symbol, *args)
> if symbol.to_s.match(/dynamic/)
> puts 'creating it'
> define_singleton_method(symbol) { puts "created #{symbol}" }
> else
> super
> end
> end
>
> end

Ignoring all of the rspec/testing aspects of this... Every time you hit
your method_missing and generate a new singleton method on an instance
you're blowing out your method cache. It pretty much does the opposite of
what you want to do. I highly recommend that you take a less clever /
dynamic approach and declare your methods ahead of time.

Hi Abinoam,
Yes your example does seem to highlight the issue behind the test. I
worked around it by checking id's of the returned object instead.

I still have the issue with the method cache but I'll have to put some
more thought into what I want to do about that problem.

Thanks,
Raj

···

On 04/15, Abinoam Jr. wrote:

Hi Raj Sahae,

In order to test how many times a method was called, RSpec defines its
own singleton_method version of it. I think this could be causing your
problem.

require_relative 'a'

describe A do
  it 'creates a dynamic method' do
    expect(subject.respond_to? :dynamic_method).to eq false
    subject.dynamic_method
    expect(subject.respond_to? :dynamic_method).to eq true
  end

  it 'only creates the dynamic method once' do

    # CHECK here how the expectation changes the singleton methods list
    STDOUT.puts subject.singleton_methods.inspect
    expect(subject).to receive(:define_singleton_method).once
    STDOUT.puts subject.singleton_methods.inspect
    STDOUT.puts subject.method(:define_singleton_method).source_location.inspect

    2.times { subject.dynamic_method }
  end
end

The output here is

# => An empty array before the expectation definition

# A "fake" define_singleton_method after the expectation definition.
# The "fake" one comes from RSpec itself.

[:define_singleton_method]
["/home/abinoam/.rvm/gems/ruby-2.1.5/gems/rspec-mocks-3.2.1/lib/rspec/mocks/method_double.rb",
59]

Best regards,
Abinoam Jr.

On Tue, Apr 14, 2015 at 9:18 PM, Raj Sahae <rajsahae@gmail.com> wrote:
> Hi Ryan,
>
> That is definitely a problem I didn't consider (and wish I had). However,
> the nature of this application makes creating all the methods in a regular
> sort of way an inordinate amount of work, and very un-maintainable. Any
> suggestions on a better way to do this?
>
> The use case here is that each sub object is associated with a string path
> '/some/object/path'. Querying a service with this path produces some data
> that includes information about subpaths. These subpaths may change based on
> the external system during runtime so I'm attempting to account for that
> with dynamic methods.
>
> Thanks,
> Raj
>
> On Tue, Apr 14, 2015 at 3:44 PM Ryan Davis <ryand-ruby@zenspider.com> wrote:
>>
>>
>> > On Apr 14, 2015, at 12:48, Raj Sahae <rajsahae@gmail.com> wrote:
>> >
>> > class A
>> >
>> > private
>> > def method_missing(symbol, *args)
>> > if symbol.to_s.match(/dynamic/)
>> > puts 'creating it'
>> > define_singleton_method(symbol) { puts "created #{symbol}" }
>> > else
>> > super
>> > end
>> > end
>> >
>> > end
>>
>> Ignoring all of the rspec/testing aspects of this... Every time you hit
>> your method_missing and generate a new singleton method on an instance
>> you're blowing out your method cache. It pretty much does the opposite of
>> what you want to do. I highly recommend that you take a less clever /
>> dynamic approach and declare your methods ahead of time.
>>
>