38

模拟框架网站中给出的大多数示例都是模拟接口。假设我目前正在使用的 NSubstitute,他们所有的模拟示例都是模拟接口。

但实际上,我看到了一些开发人员模拟具体类。是否建议模拟具体课程?

4

5 回答 5

81

理论上,模拟一个具体的类绝对没有问题;我们正在针对逻辑接口(而不是关键字interface)进行测试,并且该逻辑接口是否由 aclass或提供并不重要interface

在实践中,.NET/C# 使这有点问题。正如您提到的 .NET 模拟框架,我假设您仅限于此。

在 .NET/C# 中,默认情况下成员是非虚拟的,因此任何基于代理的模拟行为方法(即从类派生,并覆盖所有成员以执行特定于测试的内容)都将不起作用,除非您明确标记成员作为virtual. 这导致了一个问题:您正在使用一个模拟类的实例,该实例在您的单元测试中是完全安全的(即不会运行任何真实代码),但除非您确定一切正常,否则您virtual最终可能会得到真实和模拟代码的混合运行(如果有构造函数逻辑,这可能会特别成问题,它总是运行,并且如果有其他具体的依赖关系需要新建,则更加复杂)。

有几种方法可以解决这个问题。

  • 使用interfaces. 这是可行的,也是我们在NSubstitute 文档中建议的方法,但缺点是可能会使用实际上不需要的接口使您的代码库膨胀。可以说,如果我们在代码中找到好的抽象,我们自然会得到可以测试的整洁、可重用的接口。我还没有看到它像那样成功,但是 YMMV。:)
  • 努力使一切虚拟化。一个有争议的缺点是我们建议所有这些成员都旨在成为我们设计中的扩展点,而我们真的只想改变整个类的行为以进行测试。它也不会停止构造函数逻辑的运行,如果具体类需要其他依赖项,它也无济于事。
  • 通过Fody的Virtuosity 插件之类的东西使用程序集重写,您可以使用它来将程序集中的所有类成员修改为虚拟的。
  • 使用基于非代理的模拟库,例如TypeMock (付费)JustMock (付费)Microsoft Fakes (需要 VS Ultimate/Enterprise,尽管其前身Microsoft Moles是免费的)Prig (免费 + 开源)。我相信这些能够模拟类的所有方面,以及静态成员。

反对最后一个想法的一个常见抱怨是您正在通过“假”接缝进行测试;我们正在超越通常用于扩展代码的机制来改变我们代码的行为。需要超越这些机制可能表明我们的设计存在刚性。我理解这个论点,但我见过创建另一个接口的噪音超过好处的情况。我想这是意识到潜在的设计问题的问题。如果您不需要来自测试的反馈来突出设计刚性,那么它们是很好的解决方案。

我要抛出的最后一个想法是在我们的测试中改变单元的大小。通常我们有一个单一的类作为一个单元。如果我们有许多内聚的类作为我们的单元,并且有接口作为围绕该组件的明确定义的边界,那么我们可以避免模拟尽可能多的类,而只是在更稳定的边界上模拟。这可以使我们的测试变得更加复杂,其优势在于我们正在测试一个内聚的功能单元,并被鼓励围绕该单元开发可靠的接口。

希望这可以帮助。

于 2012-08-29T22:52:09.713 回答
12

更新

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

注意:不会为类创建递归替换,因为创建和使用类可能会产生潜在的副作用

.

于 2013-03-01T18:09:01.563 回答
1

问题是:为什么不呢?

我可以想到几个有用的场景,例如:

具体类的实现还没有完成,或者做它的人不可靠。所以我模拟了指定的类,并针对它测试我的代码。

模拟执行数据库访问等操作的类也很有用。如果您没有测试数据库,您可能希望为测试返回始终不变的值(通过模拟类很容易)。

于 2012-08-29T09:11:30.693 回答
0

不是不推荐,是如果你别无选择,你可以这样做。

通常设计良好的项目依赖于为单独的组件定义接口,因此您可以通过模拟其他组件来单独测试它们中的每一个。但是,如果您正在使用不允许更改的遗留代码/代码并且仍然想测试您的类,那么您别无选择,也不能因此受到批评(假设您努力尝试将这些组件切换到接口并被剥夺了这样做的权利)。

于 2012-08-29T09:09:51.190 回答
0

假设我们有:

class Foo {
    fun bar() = if (someCondition) {
        “Yes”
    } else {
        “No”
    }
}

没有什么能阻止我们在测试代码中进行以下模拟:

val foo = mock<Foo>()
whenever(foo.bar()).thenReturn(“Maybe”)

问题是它设置了类 Foo 的不正确行为。Foo 类的真实实例将永远无法返回“Maybe”。

于 2020-06-01T22:14:26.410 回答