RSpec:我怎么能不对在我调用的函数中实例化的对象使用“allow_any_instance_of”?

RSpec: How can I not use `allow_any_instance_of` for objects that get instantiated in the functions I call?

我有一个 class A 和一个方法 M,我想为其编写一个测试 T。问题是方法 M 创建了一个新对象 O。我想模拟那个新对象 O.

的方法 F
class A

  def M(p1, p2)
    @o = O.new(p1, p2)
  end

end

class O

  def F(q)
    ...
  end

end

我可以很容易地使用 RSpec 的 allow_any_instance_of 功能来做到这一点,但我真的没有看到仅使用 allow 或 [=24= 就可以做到这一点的方法].我知道我可以模拟现有实例和 class 的方法,但是根据我的测试,我无法让它与在我正在测试的方法中创建的对象的方法一起工作。

T :process do
  it "works" do
    # This works
    allow_any_instance_of(O).to receive(:F).and_return(123)
    ...
  end

  it "does not works" do
    # This fails
    allow(O).to receive(:F).and_return(123)
    ...
  end
end

我怎么知道它失败了?

我用 puts() 更改了我的 F 方法,当我使用 allow(O) 时,我可以在屏幕上看到该输出。当我使用 allow_any_instance_of() 时它根本不出现。所以我知道它仅在后者中按预期工作。

  def F(q)
    puts("If I see this, then F() was not mocked properly.")
    ...
  end

我认为 allow(O)... 应该连接到 class 所以每当创建一个新实例时,模拟函数就会随之而来,但显然不是。

您是否有 RSpec 测试以不涉及使用 allow_any_instance_of() 函数的不同方式处理此类模拟案例?

我问的原因是因为它是 marked as obsolete (@allow-old-syntax) 自 RSpec 3.3 以来,所以听起来我们不应该再使用此功能,尤其是一旦 RSpec4.x出来了,估计就没了

这个原因

allow(O).to receive(:F).and_return(123)

不起作用是因为 :F 不是 O 的方法,所以 O 永远不会收到此消息(方法调用)。

最适合您的解决方案是重构您的代码以使用依赖项注入。 (请注意,您的示例抽象到了极致,如果您提供了一个现实生活中的示例 - 更接近地面 - 一些更好的重构可能是可能的)

class A
  attr_accessor :o_implementation

  def initialize(o_implementation)
    @o_implementation = o_implementation
  end

  def M(p1, p2)
    @o = o_implementation.new(p1, p2)
  end

end

RSpec.describe A do
  subject { described_class.new(klass) }
  let(:klass) { O }
  let(:a_double) { instance_double(klass) }


  it do 
     allow(klass).to receive(:new).and_return(a_mock)
     allow(a_double).to receive(:F).and_return(123)
  end
end

通过依赖注入,您可以脱离 class 实例化的决定。这解耦了您的代码(A 不再与 O 耦合,现在它仅依赖于它正在使用的 O 接口),并使其更容易*测试。

(*) 有人可能会争辩说 allow_any_instance 更容易(更少涉及,更少打字),但它有 some issues,应尽可能避免使用。

(顺便说一句:我可以理解可能需要对代码进行非常彻底的匿名化,但您仍然可以遵循 ruby 风格指南:方法以小写字母开头,仅 classes 以大写开头)

所以首先:allow(O) 有效,但只会捕获 class 方法。如果需要捕获实例方法,则需要针对特定​​实例调用allow

由于您的示例非常稀疏,我看不出为什么我们不能将对象的创建与测试分开?如果可能的话,一个非常简单的方法是编写如下内容:

describe :process do 
  before do 
    @o = A.o_maker(p1,p2) 
    allow(@o).to receive(:some_function) { 123 } 
  end 
  it "works" do 
    # do something with `@o` that should call the function
  end
end 

与之前建议的创建模拟 class 相比,我个人更喜欢这种方法。 这可能是众所周知的,但为了清楚起见:模拟 class imho 的问题是您不再测试 class A 而是模拟。这在某些情况下可能有用,但从最初的问题来看,不清楚它是否适用于这种情况,以及这是否没有不必要的复杂性。其次:如果您的代码 如此复杂(例如,一些创建新对象然后调用 F 的方法),我宁愿 1) 重构我的代码以使其测试-able, and/or 2) 测试副作用(例如 F 添加审计日志行,设置状态,...)。我不需要 "test" 我的实现(是否调用了正确的方法),但它是否执行了(当然,一如既往,有例外,例如调用外部服务或其他东西时——但同样,所有这些都是不可能的从原来的问题中推导出来)。