我使用 NMock2,并且起草了以下 NMock 类来表示一些常见的模拟框架概念:

  • Expect:这指定了模拟方法应该返回什么,并表示调用必须发生,否则测试失败(当伴随着调用时) VerifyAllExpectationsHaveBeenMet()).

  • Stub:这指定了模拟方法应该返回的内容,但不会导致测试失败。

那么我什么时候应该做什么?

有帮助吗?

解决方案

许多模拟框架使模拟和存根的概念越来越接近,以至于它们在功能上几乎可以被认为是相同的。然而,从概念的角度来看,我通常尝试遵循这个约定:

  • 嘲笑:仅当您明确尝试验证被测对象的行为时(即您的测试表明该对象必须调用该对象)。
  • 存根:当您尝试测试某些功能/行为时,但为了使其正常工作,您需要依赖一些外部对象(即您的测试表明该对象必须执行某些操作,但作为副作用,它可能会调用该对象)

当您确保每个单元测试只测试一件事时,这一点就会变得更加清晰。当然,如果您尝试在一次测试中测试所有内容,那么您不妨期待所有内容。但是,通过仅期望特定单元测试要检查的内容,您的代码会更加清晰,因为您可以一眼看出测试的目的是什么。

这样做的另一个好处是,您将稍微更好地免受更改的影响,并在更改导致中断时获得更好的错误消息。换句话说,如果您巧妙地更改实现的某些部分,则更有可能只破坏一个测试用例,这将准确地向您显示破坏的内容,而不是破坏一整套测试并产生噪音。

编辑:基于一个人为的示例,它可能会更清楚,其中计算器对象审核数据库中的所有添加内容(以伪代码)......

public void CalculateShouldAddTwoNumbersCorrectly() {
    var auditDB = //Get mock object of Audit DB
    //Stub out the audit functionality...
    var calculator = new Calculator(auditDB);
    int result = calculator.Add(1, 2);
    //assert that result is 3
}

public void CalculateShouldAuditAddsToTheDatabase() {
    var auditDB = //Get mock object of Audit DB
    //Expect the audit functionality...
    var calculator = new Calculator(auditDB);
    int result = calculator.Add(1, 2);
    //verify that the audit was performed.
}

因此,在第一个测试用例中,我们正在测试 Add 方法并且不关心审计事件是否发生,但我们碰巧知道计算器在没有auditDB引用的情况下无法工作,因此我们只是将其存根以给我们提供最少的功能来获取特定的测试用例在职的。在第二个测试中,我们专门测试当您执行 Add, ,审计事件发生,所以这里我们使用期望(请注意,我们甚至不关心结果是什么,因为这不是我们正在测试的)。

是的,您可以将这两种情况合并为一种,并进行期望并断言您的结果是 3,但随后您将在一个单元测试中测试两种情况。这将使您的测试变得更加脆弱(因为有更大的表面积可能会改变以破坏测试)并且不太清晰(因为当合并测试失败时,问题是什么并不立即显而易见。是添加不起作用,还是审计不起作用?)

其他提示

“预期操作,存根查询”。如果调用应该改变被测试对象之外的世界状态,那么就将其作为期望——您关心它是如何被调用的。如果它只是一个查询,您可以在不更改系统状态的情况下调用它一次或六次,然后存根调用。

还有一件事,请注意存根和期望之间的区别,即单个调用,不一定是整个对象。

出色地...恕我直言,这再简单不过了:如果您的测试是为了确保 Presenter 调用 Save,请执行 Expect。如果您的测试是为了确保您的 Presenter 在 Save 抛出异常时能够优雅地处理异常,请执行存根。

欲了解更多详情,请查看 Hanselman 和 Osherove 的播客 (《单元测试的艺术》作者)

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top