模拟框架网站中给出的大多数示例都是模拟接口。假设我目前正在使用的 NSubstitute,他们所有的模拟示例都是模拟接口。
但实际上,我看到了一些开发人员模拟具体类。是否建议模拟具体课程?
模拟框架网站中给出的大多数示例都是模拟接口。假设我目前正在使用的 NSubstitute,他们所有的模拟示例都是模拟接口。
但实际上,我看到了一些开发人员模拟具体类。是否建议模拟具体课程?
理论上,模拟一个具体的类绝对没有问题;我们正在针对逻辑接口(而不是关键字interface
)进行测试,并且该逻辑接口是否由 aclass
或提供并不重要interface
。
在实践中,.NET/C# 使这有点问题。正如您提到的 .NET 模拟框架,我假设您仅限于此。
在 .NET/C# 中,默认情况下成员是非虚拟的,因此任何基于代理的模拟行为方法(即从类派生,并覆盖所有成员以执行特定于测试的内容)都将不起作用,除非您明确标记成员作为virtual
. 这导致了一个问题:您正在使用一个模拟类的实例,该实例在您的单元测试中是完全安全的(即不会运行任何真实代码),但除非您确定一切正常,否则您virtual
最终可能会得到真实和模拟代码的混合运行(如果有构造函数逻辑,这可能会特别成问题,它总是运行,并且如果有其他具体的依赖关系需要新建,则更加复杂)。
有几种方法可以解决这个问题。
interfaces
. 这是可行的,也是我们在NSubstitute 文档中建议的方法,但缺点是可能会使用实际上不需要的接口使您的代码库膨胀。可以说,如果我们在代码中找到好的抽象,我们自然会得到可以测试的整洁、可重用的接口。我还没有看到它像那样成功,但是 YMMV。:)反对最后一个想法的一个常见抱怨是您正在通过“假”接缝进行测试;我们正在超越通常用于扩展代码的机制来改变我们代码的行为。需要超越这些机制可能表明我们的设计存在刚性。我理解这个论点,但我见过创建另一个接口的噪音超过好处的情况。我想这是意识到潜在的设计问题的问题。如果您不需要来自测试的反馈来突出设计刚性,那么它们是很好的解决方案。
我要抛出的最后一个想法是在我们的测试中改变单元的大小。通常我们有一个单一的类作为一个单元。如果我们有许多内聚的类作为我们的单元,并且有接口作为围绕该组件的明确定义的边界,那么我们可以避免模拟尽可能多的类,而只是在更稳定的边界上模拟。这可以使我们的测试变得更加复杂,其优势在于我们正在测试一个内聚的功能单元,并被鼓励围绕该单元开发可靠的接口。
希望这可以帮助。
更新:
3年后我想承认我改变了主意。
从理论上讲,我仍然不喜欢仅仅为了方便创建模拟对象而创建接口。在实践中(我正在使用 NSubstitute)它更容易使用,Substitute.For<MyInterface>()
而不是模拟具有多个参数的真实类,例如Substitute.For<MyCLass>(mockedParam1, mockedParam2, mockedParam3)
,每个参数应该单独模拟。NSubstitute 文档中描述了其他潜在问题
在我们公司,现在推荐的做法是使用接口。
原始答案:
如果您不需要创建同一抽象的多个实现,请不要创建接口。正如David Tchepak 所指出的那样,您不想使用实际上可能不需要的接口来膨胀您的代码库。
来自 http://blog.ploeh.dk/2010/12/02/InterfacesAreNotAbstractions.aspx
您是否从类中提取接口以启用松散耦合?如果是这样,您的接口和实现它们的具体类之间可能存在 1:1 的关系。这可能不是一个好兆头,并且违反了 重用抽象原则 (RAP)。
只有一个给定接口的实现是一种代码异味。
如果您的目标是可测试性,我更喜欢David Tchepak 上面的回答中的第二个选项。
但是,我不相信您必须将所有内容都虚拟化。仅将要替换的方法设为虚拟就足够了。我还将在方法声明旁边添加一条注释,该方法是虚拟的,只是为了使它可以替代单元测试模拟。
但是请注意,替换具体类而不是接口有一些限制。例如对于 NSubstitute
注意:不会为类创建递归替换,因为创建和使用类可能会产生潜在的副作用
.
问题是:为什么不呢?
我可以想到几个有用的场景,例如:
具体类的实现还没有完成,或者做它的人不可靠。所以我模拟了指定的类,并针对它测试我的代码。
模拟执行数据库访问等操作的类也很有用。如果您没有测试数据库,您可能希望为测试返回始终不变的值(通过模拟类很容易)。
不是不推荐,是如果你别无选择,你可以这样做。
通常设计良好的项目依赖于为单独的组件定义接口,因此您可以通过模拟其他组件来单独测试它们中的每一个。但是,如果您正在使用不允许更改的遗留代码/代码并且仍然想测试您的类,那么您别无选择,也不能因此受到批评(假设您努力尝试将这些组件切换到接口并被剥夺了这样做的权利)。
假设我们有:
class Foo {
fun bar() = if (someCondition) {
“Yes”
} else {
“No”
}
}
没有什么能阻止我们在测试代码中进行以下模拟:
val foo = mock<Foo>()
whenever(foo.bar()).thenReturn(“Maybe”)
问题是它设置了类 Foo 的不正确行为。Foo 类的真实实例将永远无法返回“Maybe”。