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.
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?
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
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?
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.
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
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.
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.
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.
>>
>