您应该在单元测试中使用 Moq 提供的 "Verify" 和 "VerifyAll" 方法吗?

Should you use the "Verify" and "VerifyAll" methods provided by Moq in your Unit Tests?

似乎将它们用作确定被测方法正确执行是否适得其反的方法,因为它会导致脆弱的测试。换句话说,您将测试与实现联系在一起。因此,如果您以后想要更改实现,您也将不得不更改测试。我问这个问题是因为我受过训练,在每个单元测试中总是至少使用其中一种方法,我想我可能刚刚顿悟这实际上是一种非常糟糕的做法。

首选方式是使用 AAA。但是对于具有 void return 类型的外部依赖项(比如 void WriteData(Data data)),Verify 可能很有用(或先设置,然后 VerifyAll)。

首先,重要的是要了解 Verify 系列方法的存在是有原因的——它们允许您测试 unobservable1 行为 你的系统。那是什么意思?考虑应用程序生成和发送报告的简单示例。您的最终组件很可能如下所示:

public void SendReport(DateTime reportDate, ReportType reportType)
{
    var report = generator.GenerateReport(reportDate, reportType);
    var reportAsPlainText = converter.ConvertReportToText(report);
    reportSender.SendEmailToSubscribers(body: reportAsPlainText);
}

你如何测试这个方法?它没有 return 任何东西,因此您无法检查值。它不会改变系统的状态(例如,翻转一些标志),因此您也无法检查它。调用 SendReport 的唯一可见结果是报告是通过 SendEmailToSubscribers 调用发送的。这是 SendReport 方法的主要职责——这是单元测试应该验证的内容。

当然,您的单元测试不应该也不会检查某些电子邮件是否已发送或送达。您将验证 reportSender 的 mock。这就是您使用 Verify 方法的地方。检查是否确实发生了对某些模拟的调用。

最后一点,Roy Osherove 在他的书中 Art Of Unit Testing (2nd edition) 根据 可以检查的内容 :

将单元测试分为三类
  • return 方法值(简单、通用)
  • 系统状态变化(简单,罕见)
  • 调用外部组件(复杂,罕见)

最后一类是您在其上使用模拟和 Verify 方法的地方。对于其他两个,存根就足够了(Setup 方法)。

当您的代码设计正确时,此类测试(最后一类)在您的代码库中占少数,大约在 5% - 10% 的范围内(数字取自 Roy 的书,与我的观察结果一致)。


1:不可观察,因为调用者无法轻易验证调用后究竟发生了什么。

基于模拟的测试

围绕单元测试中模拟的脆弱性以及它们是否是一件好事存在很多争论。我个人认为这是您必须在可维护性和健壮性之间做出的权衡。您将生产代码放在 unit test pressure 下的次数越多,使用模拟对其进行隔离测试,实现通过测试的可能性就越小。因此,您可以强制您的生产代码具有稳健性和良好的设计。另一方面,它确实将自己绑定到特定的实现并增加了维护负担,因为一旦实现细节发生变化,就必须更改更多测试。

VerifyAll() 语法

这主要是个人喜好问题,但我发现 VerifyAll() 并不能揭示意图,即当您阅读测试套件时,您希望仅通过查看断言就可以很好地了解规范, VerifyAll() 没有任何意义。 即使我编写基于模拟的测试,我也更喜欢使用特定断言失败消息的Arrange Act Assert 方法。它比包罗万象的 VerifyAll() 调用更清晰、更少 "magical"。

在每个测试方法中使用 VerifyAll()

最好的情况是矫枉过正,最坏的情况是会损坏您的测试套件。

  • 作为一般规则,单元测试应该只测试一件事。除了正常的断言之外,系统地调用 VerifyAll() 会带来混乱——如果测试失败,您无法确定哪里出了问题。

  • 就可读性而言,您只是在为每个测试添加噪音。仅通过阅读测试方法来追溯VerifyAll()真正意味着什么是非常困难的。

  • 您通常希望选择在何处使用模拟对您的实现施加设计压力,而不是盲目地在所有地方应用它,因为它需要付出维护代价。

因此,如果您真的必须使用 VerifyAll(),IMO 最好为它编写单独的测试。