是否建议模拟混凝土类?

时间:2012-08-29 09:03:56

标签: c# unit-testing mocking nsubstitute

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

但实际上,我看到一些开发人员嘲笑混凝土类。是否建议模拟具体类?

5 个答案:

答案 0 :(得分:68)

理论上,嘲笑具体的课程绝对没有问题;我们正在针对逻辑接口(而不是关键字interface)进行测试,并且该逻辑接口是由class还是interface提供并不重要。

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

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

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

  • 使用interfaces。这是有效的,也是我们在NSubstitute documentation中提出的建议,但有一个缺点,即可能实际上不需要的接口可能使代码库膨胀。可以说,如果我们在代码中找到好的抽象,我们自然会得到我们可以测试的整洁,可重用的接口。我还没有看到它像这样,但是YMMV。 :)
  • 努力绕过一切虚拟。一个可论证的缺点是,当我们真的只想改变整个类的行为以进行测试时,我们建议所有这些成员都是我们设计中的扩展点。它也不会停止构造函数逻辑运行,如果具体类需要其他依赖项,它也没有帮助。
  • 使用Virtuosity add-inFody之类的程序集重写,您可以使用它来修改程序集中的所有类成员是虚拟的。
  • 使用基于非代理的模拟库,例如TypeMock (付费)JustMock (付费)Microsoft Fakes < em>(需要VS Ultimate / Enterprise,虽然它的前身Microsoft Moles是免费的)或Prig (免费+开源)。我相信这些能够模拟类的所有方面,以及静态成员。

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

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

希望这有帮助。

答案 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”。