可重复使用的模拟与每次测试中的模拟

时间:2011-01-07 17:20:13

标签: unit-testing mocking moq

我们的团队正在进入TDD并努力进行单元测试的最佳实践。我们的测试代码使用依赖注入。我们的测试通常遵循Arrange-Act-Assert类型的布局,我们使用Moq模拟Arrange部分中的依赖关系。

理论上,单元测试应该是一个屏蔽,可以在你重构时保护你。但它正在变成一个阻止我们这样做的锚。我正在努力确定我们的流程失败的地方。

考虑简化的例子:

  • XRepository.Save更改了签名和行为/合同。
  • XController.Save使用XRepository.Save,因此它被重构为使用新接口。但在外部,它的公共合同没有改变。

我希望控制器测试 not 不需要重构,而是向我证明我的新控制器实现符合未更改的合同。但我们在这里失败了,因为事实并非如此。

每个控制器测试都会动态地模拟存储库接口。他们都需要改变。此外,由于每个测试都不想模拟所有接口和方法,我们发现我们的测试与特定实现相关联,因为它需要知道要模拟的方法。

对于我们拥有的更多测试,重构会变得更加困难!或者更确切地说,我们模拟界面的次数越多。

所以我的问题:

  1. 是否愿意在每个测试中使用动态模拟,而不是为每个界面制作可重复使用的手工模拟?

  2. 鉴于我的故事,我是否遗漏了一些原则或陷入了常见的陷阱?

  3. 谢谢!

2 个答案:

答案 0 :(得分:10)

你不缺少任何原则,但这是一个常见的问题。我认为每个团队都以自己的方式解决(或不解决)。

副作用

对于任何有副作用的功能,您将继续遇到此问题。我找到了副作用函数,我必须进行测试以确保以下部分或全部:

  • 它是/未被称为
  • 被叫的次数
  • 传递给它的论据
  • 通话顺序

在测试中确保这一点通常意味着违反封装(我与实现交互并了解)。无论何时执行此操作,您都将隐式地将测试与实现相结合。这将导致您必须在更新正在公开/测试的实现部分时更新测试。

可重复使用的模拟

我使用了可重复使用的模拟效果很好。权衡取舍是它们的实施更复杂,因为它需要更加完整。您确实可以降低更新测试的成本以适应重构。

接受TDD

另一种选择是改变您正在测试的内容。由于这实际上是关于改变您的测试策略,因此不能轻易进入。您可能希望先进行一些分析,看看它是否真的适合您的情况。

我以前用单元测试做TDD。我遇到了一些我认为不应该处理的问题。特别是在重构周围,我注意到我们通常不得不更新许多测试。这些重构不是代码单元,而是主要组件的重组。我知道很多人会说问题是频繁的大变化,而不是单元测试。对于我们的规划/架构部分导致的大变化,可能有一些道理。但是,业务决策也导致了方向的变化。这些和其他合法原因导致需要对代码进行大量更改。最终结果是,由于所有测试更新,大型重构变得更加缓慢和痛苦。

由于单元测试未涵盖的集成问题,我们也遇到了错误。我们通过手动验收测试做了一些。我们实际上做了很多工作,使验收测试尽可能低。它们仍然是手动的,我们觉得在单元测试和验收测试之间存在很多交叉,应该有一种方法可以降低实现两者的成本。

然后该公司裁员。突然间,我们没有相同的资源来投入编程和维护。我们被推动为我们所做的一切获得最大的回报,包括测试。我们首先添加了所谓的部分堆栈测试,以涵盖我们遇到的常见集成问题。事实证明它们非常有效,我们开始进行不那么经典的单元测试。我们还摆脱了手动验收测试(Selenium)。我们慢慢推高测试开始测试的地方,直到我们基本上进行验收测试,但没有浏览器。我们将模拟GET,POST或PUT方法到特定的控制器并检查验收标准。

  • 数据库已正确更新
  • 返回了正确的HTTP状态代码
  • 返回的页面是:
    • 是有效的HTML 4.01严格
    • 包含我们要发送给用户的信息

我们结束了更少的错误。特别是几乎所有的集成错误,以及由于大型重构导致的错误几乎完全消失。

有权衡。事实证明,专业人士远远超过了出局的缺点。缺点:

  • 测试通常更复杂,几乎每个人都测试一些副作用。
  • 我们可以判断什么时候出现故障,但它不像单元测试那样有针对性,所以我们必须做更多的调试来追踪问题所在。

答案 1 :(得分:2)

我自己也在努力解决这类问题,并且没有一个我觉得可靠的答案,但这里有一种初步的思考方式。我观察了两种单元测试

  1. 有一些测试可以运用公共界面,如果我们要自信地重构,这些非常重要,它们证明我们尊重我们与客户的合同。这些测试最好由手工制作的可重复使用的模拟服务,该模拟处理一小部分测试数据。
  2. 有“覆盖”测试。这些往往是为了证明我们的实现在依赖性行为不当时表现正常。我认为这些需要在飞行模拟中激发特定的实现路径。