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