将类/模块恢复到原始状态

Jay*_*rka 5 ruby rspec

我想知道 RSpec 如何实现这一点:

expect( MyObject ).to receive(:the_answer).and_return(42)
Run Code Online (Sandbox Code Playgroud)

不RSpec的利用define_singleton_method注入:the_answerMyObject

那可以工作。让我们假设这是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.dupdefine_singleton_method使用之前运行,但是,我如何恢复old_objectObject

当我想做的只是恢复一个类方法时,这也没有让我感到高效。虽然目前这不是一个问题,但也许将来我还想保持某些实例变量MyObject完好无损。是否有一种非hacky 的方式来复制代码 ( 21 * 2) 并将其重新定义为the_answerviadefine_singleton_method而不是完全替换Object

欢迎所有想法,我绝对想知道如何制作Object===old_object无论如何。

Ste*_*fan 4

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 的实际代码要复杂得多,但您明白了。

  • @JayTarka,如果您将“MyObject”替换为“MyObject.singleton_class”,例如“MyObject.singleton_class.instance_method(:the_answer)”,我的示例应该适用于类方法。 (2认同)