首页 > 解决方案 > 在代码中(而不是在测试中)使用 NSubstitute(或其他允许模拟的测试框架)是一种好习惯吗?

问题描述

我面临着在业务逻辑中使用 NSubstitute(在测试类之外):

var extension = Substitute.For<IExtension>();

当您需要模拟某些类(接口)时,我习惯于在测试类中使用 NSubstitute。但是在测试类之外使用 NSubstitute 让我很困惑。这是正确的地方吗?使用像依赖注入容器这样可以创建接口/类实例的 NSubstitute 是否正确?

我担心的是 NSubstitute 被设计用于测试。测试中的性能并不是很重要的事情,这就是它可能很慢的原因。此外,它依赖于反射,所以它不能很快。但是,NSubstitute 的性能很差,还是可以?

还有其他原因,为什么不应在测试之外使用 NSubstitute 或其他模拟库?

标签: c#.netunit-testingmockingnsubstitute

解决方案


不,在生产代码中使用模拟库通常不是一个好习惯。(我将在这里大量使用“一般”,因为我认为任何关于“良好实践”的问题都需要一定程度的概括。人们可能会提出反对这种概括的案例,但我认为这些案例将是绝大多数。)

即使没有性能考虑,模拟库也会为接口/类创建测试实现。测试实现通常支持诸如记录调用和存根特定调用以返回特定结果等功能。一般来说,当我们有一个用于生产代码的接口或类时,它是为了实现一些特定的目的,而不是记录调用和存根返回值的通用目的。

虽然可以使用 NSubstitute 为接口提供特定实现并存根每个调用以执行生产代码逻辑,但为什么不改为创建具有所需实现的类呢?

这通常具有以下优点:

  • 应该更简洁地实现(如果没有,请考虑切换到更好的语言!:D)
  • 使用您的编程语言的本机结构
  • 应该有更好的性能(删除模拟库所需的间接级别)

对于 NSubstitute 特别是有一些重要的原因为什么你不应该在生产代码中使用它。因为该库是为测试代码设计的,所以它使用了一些生产代码不可接受的方法:

  • 它使用全局状态来支持其语法。
  • 它滥用 C#/VB 语法进行测试(几乎可以认为是测试 DSL)。例如说sub.MyCall()返回一个int. 像这样的调用存根sub.MyCall().Returns(42)意味着我们正在调用int.Returns(42),现在它会以某种方式影响调用的返回值之外的int调用。这与 C#/VB 通常的工作方式完全不同。
  • 一切都需要虚拟会员。许多模拟库共享此约束。对于 NSubstitute,如果将其与非虚拟成员一起使用,可能会得到不可预知的结果。对于生产代码来说,不可预测性并不是一件好事。
  • 测试通常是短暂的。NSubstitute(可能还有其他库)可以做出依赖于短期对象的实现决策。
  • 测试显示特定案例的通过或失败。这意味着如果 NSubstitute 出现问题,可以在尝试编写和运行特定测试时立即发现它。虽然在 NSubstitute 的质量和可靠性方面付出了很多努力,但 C# 编译器所经历的工作量和审查却是完全不同的水平。对于生产代码,我们需要一个非常稳定的基础作为基础。

总之:您的编程语言提供了为实现逻辑接口而设计和优化的结构。您的模拟库提供了为更有限的任务设计和优化的结构,即提供逻辑接口的测试实现以与测试代码一起使用,使其与依赖项隔离。除非你有一个铁定的理由来解释为什么你会做相当于用一张纸而不是铲子挖洞的编程,否则我建议将每个工具用于其预期目的。:)


推荐阅读