I am wondering how RSpec achieves this:
expect( MyObject ).to receive(:the_answer).and_return(42)
Does RSpec utilize define_singleton_method
to inject :the_answer
into MyObject
?
That could work. Lets pretend this is what MyObject
looks like:
class MyObject
def self.the_answer
21 * 2
end
end
The injected the_answer
method could contain something like this:
@the_answer_called = true
42
However, how does RSpec then restore the class to its unmocked state? How does it restore it so MyObject.the_answer
"truly" returns 42 again? (Via 21 * 2
)
Since writing this question I'm thinking it doesn't. The class methods remain mocked until RSpec stops running.
However, (and this is the crux of the question) how could one undo a define_singleton_method?
I'm thinking the easiest way would be running old_object = Object.dup
before define_singleton_method
is utilized, but then, how do I restore old_object
to Object
?
That also doesn't strike me as efficient, when all I want to do is restore one class method. While this isn't a concern at the moment, maybe in the future I also want to keep certain instance variables inside MyObject
intact. Is there a non-hacky way to duplicate the code (21 * 2
) and redefine that as the_answer
via define_singleton_method
rather than completely replacing Object
?
All ideas welcome, I definitely want to know how to make Object
=== old_object
regardless.
RSpec does not duplicate / replace the instance or its class. It dynamically removes the instance method and defines a new one. Here's how it works: (you can do the same for class methods)
Given your class MyObject
and an instance o
:
class MyObject
def the_answer
21 * 2
end
end
o = MyObject.new
o.the_answer #=> 42
RSpec first saves the original method using Module#instance_method
. It returns an UnboundMethod
:
original_method = MyObject.instance_method(:the_answer)
#=> #<UnboundMethod: MyObject#the_answer>
it then removes the method using Module#remove_method
: (we have to use send
here because remove_method
is private).
MyObject.send(:remove_method, :the_answer)
and defines a new one using Module#define_method
:
MyObject.send(:define_method, :the_answer) { 'foo' }
If you call the_answer
now, you are instantly invoking the new method:
o.the_answer #=> "foo"
After the example, RSpec removes the new method
MyObject.send(:remove_method, :the_answer)
and restores the original one (define_method
accepts either a block or a method):
MyObject.send(:define_method, :the_answer, original_method)
Calling the method works as expected:
o.the_answer #=> 42
RSpec's actual code is much more complex, but you get the idea.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With