模拟框架网站中给出的大多数示例都是模拟接口。假设我正在使用的NSubstitute,他们所有的模拟示例都是模拟界面。
但实际上,我看到一些开发人员嘲笑混凝土类。是否建议模拟具体类?
答案 0 :(得分:68)
理论上,嘲笑具体的课程绝对没有问题;我们正在针对逻辑接口(而不是关键字interface
)进行测试,并且该逻辑接口是由class
还是interface
提供并不重要。
在实践中,.NET / C#使这有点问题。正如您提到的.NET模拟框架,我将假设您仅限于此。
在.NET / C#默认情况下,成员是非虚拟的,因此任何基于代理的模拟行为方法(即从类中派生,并覆盖所有成员以执行特定于测试的内容)将不起作用,除非您明确将成员标记为virtual
。这会导致一个问题:您正在使用一个模拟类的实例,该实例在单元测试中是完全安全的(即不会运行任何实际代码),但除非您确定所有内容都是virtual
你最终可能会混合运行真实代码和模拟代码(如果有构造函数逻辑,这可能会特别成问题,它总是运行,如果有其他具体的依赖项需要更新,则会更复杂。)
有几种方法可以解决这个问题。
interfaces
。这是有效的,也是我们在NSubstitute documentation中提出的建议,但有一个缺点,即可能实际上不需要的接口可能使代码库膨胀。可以说,如果我们在代码中找到好的抽象,我们自然会得到我们可以测试的整洁,可重用的接口。我还没有看到它像这样,但是YMMV。 :)对最后一个想法的一个常见抱怨是你正在通过“假”接缝进行测试;我们正在通常用于扩展代码以改变代码行为的机制之外。需要走出这些机制可能表明我们的设计具有刚性。我理解这个论点,但我已经看到了创建另一个接口的噪音超过了好处的情况。我想这是一个意识到潜在设计问题的问题;如果您不需要测试中的反馈来突出设计刚性,那么它们就是很好的解决方案。
我要抛弃的最后一个想法就是在我们的测试中改变单位的大小。通常我们将单个类作为一个单元。如果我们有许多内聚类作为我们的单元,并且接口充当了围绕该组件的明确定义的边界,那么我们可以避免必须模拟尽可能多的类,而只是模拟更稳定的边界。这可以使我们的测试变得更加复杂,其优势在于我们正在测试一个具有凝聚力的功能单元,并鼓励他们围绕该单元开发可靠的接口。
希望这有帮助。
答案 1 :(得分:10)
更新:
3年后,我想承认我改变了主意。即使在理论上我仍然不喜欢创建接口只是为了方便创建模拟对象,在实践中(我使用NSubstitute)使用Substitute.For<MyInterface>()
而不是使用多个参数模拟真实类更容易,例如Substitute.For<MyCLass>(mockedParam1, mockedParam2, mockedParam3)
,其中每个参数都应单独模拟。 NSubstitute documentation
在我们公司,现在推荐的做法是使用接口。
原始回答:
如果您不需要创建相同抽象的多个实现,请不要创建接口。 正如pointed by David Tchepak一样,您不希望使用可能实际上不需要的接口使代码库膨胀。
从http://blog.ploeh.dk/2010/12/02/InterfacesAreNotAbstractions.aspx
您是否从类中提取接口以启用松散 耦合?如果是这样,您可能有一个1:1 relationship between your interfaces and the concrete classes来实现它们。 这可能是not a good sign,违反了Reused Abstractions Principle (RAP)。
只有一个给定界面的实现是代码味道。
如果您的目标是可测试性,我更喜欢David Tchepak's answer above中的第二个选项。
然而,我不相信你必须虚拟化一切。只使用虚拟方法就可以替代。 我还将在方法声明旁边添加注释,该方法只是虚拟的,以使其可替代单元测试模拟。
但请注意,替换具体类而不是接口有一些限制。 例如。 for NSubstitute
注意:不会为类创建递归替换,如 创建和使用类可能会产生潜在的不良副作用
答案 2 :(得分:1)
问题是:为什么不呢?
我可以想到一些有用的场景,例如:
具体课程的实施尚未完成,或者实施该课程的人员不可靠。所以我按照指定的方式模拟了类,并根据它测试我的代码。
模拟执行数据库访问等操作的类也很有用。如果您没有测试数据库,您可能希望返回始终保持不变的测试值(通过模拟类很容易)。
答案 3 :(得分:0)
这不是推荐的,如果你别无选择就可以做到这一点。
通常设计良好的项目依赖于为单独的组件定义接口,因此您可以通过模拟其他组件来单独测试每个组件。但是,如果您正在使用遗留代码/代码而不允许更改并仍想测试您的课程,那么您别无选择,也不能因此而受到批评(假设您已经努力尝试将这些组件切换到接口,但被拒绝了。)
答案 4 :(得分:0)
假设我们有
val foo = mock<Foo>()
whenever(foo.bar()).thenReturn(“Maybe”)
没有什么可以阻止我们在测试代码中进行以下模拟:
loadImage(name) {
import(`../images/${name}`)
.then(image => {
console.log(image); // This will show an object with a `default` property as the image you imported
this.setState({ src: image.default })
})
}
问题在于它正在设置类Foo的错误行为。 Foo类的真实实例将永远无法返回“ Maybe”。