我想知道 RSpec 如何实现这一点:
expect( MyObject ).to receive(:the_answer).and_return(42)
Run Code Online (Sandbox Code Playgroud)
不RSpec的利用define_singleton_method
注入:the_answer
到MyObject
?
那可以工作。让我们假设这是MyObject
看起来的样子:
class MyObject
def self.the_answer
21 * 2
end
end
Run Code Online (Sandbox Code Playgroud)
该注入的 the_answer
方法可能包含这样的事情:
@the_answer_called = true
42
Run Code Online (Sandbox Code Playgroud)
但是,RSpec 如何将类恢复到未模拟状态?它如何恢复它以便MyObject.the_answer
“真正”再次返回 42 ?(通过 21 * 2
)
自从写这个问题以来,我认为它没有。类方法保持模拟直到 RSpec 停止运行。
但是,(这是问题的关键)如何撤消define_singleton_method?
我认为最简单的方法是old_object = Object.dup
在define_singleton_method
使用之前运行,但是,我如何恢复old_object
到Object
?
当我想做的只是恢复一个类方法时,这也没有让我感到高效。虽然目前这不是一个问题,但也许将来我还想保持某些实例变量MyObject
完好无损。是否有一种非hacky 的方式来复制代码 ( 21 * 2
) 并将其重新定义为the_answer
viadefine_singleton_method
而不是完全替换Object
?
欢迎所有想法,我绝对想知道如何制作Object
===old_object
无论如何。
RSpec 不会复制/替换实例或其类。它动态删除实例方法并定义一个新方法。它的工作原理如下:(您可以对类方法执行相同的操作)
给定您的班级MyObject
和实例o
:
class MyObject
def the_answer
21 * 2
end
end
o = MyObject.new
o.the_answer #=> 42
Run Code Online (Sandbox Code Playgroud)
RSpec 首先使用 保存原始方法Module#instance_method
。它返回一个UnboundMethod
:
original_method = MyObject.instance_method(:the_answer)
#=> #<UnboundMethod: MyObject#the_answer>
Run Code Online (Sandbox Code Playgroud)
然后它使用 : 删除该方法Module#remove_method
(我们必须send
在这里使用,因为remove_method
它是私有的)。
MyObject.send(:remove_method, :the_answer)
Run Code Online (Sandbox Code Playgroud)
并使用以下方法定义一个新的Module#define_method
:
MyObject.send(:define_method, :the_answer) { 'foo' }
Run Code Online (Sandbox Code Playgroud)
如果您the_answer
现在调用,您将立即调用新方法:
o.the_answer #=> "foo"
Run Code Online (Sandbox Code Playgroud)
示例之后,RSpec 删除了新方法
MyObject.send(:remove_method, :the_answer)
Run Code Online (Sandbox Code Playgroud)
并恢复原始的(define_method
接受块或方法):
MyObject.send(:define_method, :the_answer, original_method)
Run Code Online (Sandbox Code Playgroud)
调用该方法按预期工作:
o.the_answer #=> 42
Run Code Online (Sandbox Code Playgroud)
RSpec 的实际代码要复杂得多,但您明白了。