在 文档 Rspec Mocks 维护者
...出于多种原因不鼓励其使用...
列出的原因似乎与此功能的
expect
版本有关,但它们对两者没有区别。
我明白为什么
expect
版本不鼓励,但 allow
版本似乎没有列出所列出的问题。
似乎使用
allow
版本是测试某些类型行为的最便捷方法。
示例:
我正在测试某些路线的授权。一种方法是在数据库中创建一个用户,但这似乎相当严厉,我更喜欢只创建一个模拟对象并在授权调用期间返回它。此示例是使用复杂库的测试代码的具体案例,这些库是单独测试的。
鉴于不鼓励,有哪些更好的选择?
根据您发布的文档,不使用
allow_any_instance_of
的原因是它过于严厉,可能会掩盖您可能会错过的潜在代码气味。话虽如此,我仍然使用它......他们肯定没有提供替代方案,所以如果您需要该功能并且考虑过其他选项或其他方法来编写测试并决定不使用它们,那就使用它吧。
如您所知,
allow_any_instance_of
在类的每个实例上存根方法。这是危险的,因为您可能会意外地在测试中引用对象的错误实例,这可能会导致错误的结果。
但是,如果这足以让 RSpec 团队建议不要使用它,那么他们为什么要包含它呢?那么,还有什么选择呢?
有充分的理由。但是,在我到达那里之前,让我解释一下替代方案。
考虑这段代码:
class Foo
def bar
baz = Baz.new(1)
baz.qux
end
end
class Baz
def initialize(value)
@value = value
end
def qux
@value
end
end
假设我想在
qux
实例上存根 baz
方法?我可以用allow_any_instance_of
做到这一点。但是,如果我们需要在 Baz
方法中使用 Foo#bar
的多个实例怎么办?
class Foo
def bar
baz1 = Baz.new(1)
baz2 = Baz.new(2)
baz1.qux + baz2.qux
end
end
现在,当我们
allow_any_instance_of(Baz).to receive(:qux)
时,我们允许两个实例接收消息。也许这对于某些用例来说没问题。但是,更可测试的设计会将 Baz
的实例传递到 bar
方法或 Foo#initialize
方法。这种技术称为依赖注入。看起来像这样:
# constructor injection
class Foo
def initialize(baz1, baz2)
@baz1, @baz2 = baz1, baz2
end
def bar
@baz1.qux + @baz2.qux
end
end
# calling code
Foo.new(Baz.new(1), Baz.new(2)).bar
现在,您的测试可以传递实际的
Baz
对象(如果它们是轻量级的)或单独模拟它们(如果它们很昂贵),而不是使用 allow_any_instance_of
的粗棒。
那么,为什么人们不总是使用依赖注入呢?
事实证明,某些框架使这成为不可能。例如,Ruby on Rails 向开发人员隐藏了控制器对象的创建。因此,没有机会创建初始化程序并将依赖项注入控制器。因此,您必须在控制器方法本身中创建任何依赖项,这使您除了使用
allow_any_instance_of
之外别无选择。
(嗯,从技术上讲,您还可以存根 Baz.new 以返回存根 qux 方法的 instance_double。但是,现在您已经全部完成了实现,这使得重构该代码变得更加困难,因为您将破坏您的所有也不要这样做。)
最后,遗留代码(未经测试的代码)通常是在没有依赖注入的情况下编写的。因此,
allow_any_instance_of
允许您在开始重构为更可测试的实现之前对现有代码进行测试。