我使用 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
方法的功能,并且不关心是否发生审计事件,但我们碰巧知道计算器在没有审计数据库引用的情况下无法工作,所以我们只是存根它为我们提供最少的功能以使我们的特定测试用例正常工作。在第二个测试中,我们专门测试当您执行 时Add
,审计事件会发生,所以这里我们使用期望(请注意,我们甚至不关心结果是什么,因为这不是我们要测试的)。
是的,您可以将这两个案例合二为一,并做出预期并断言您的结果是 3,但是您在一个单元测试中测试了两个案例。这会使您的测试更脆弱(因为有更大的表面积可能会改变以破坏测试)并且不太清晰(因为当合并的测试失败时,问题不是很明显......添加不起作用,还是审计不起作用?)
“预期操作,存根查询”。如果调用应该改变被测对象之外的世界的状态,那么让它成为一个期望——你关心它是如何被调用的。如果它只是一个查询,你可以调用它一次或六次而不改变系统的状态,然后存根调用。
还有一件事,请注意存根和期望之间的区别,即单个调用,不一定是整个对象。
嗯......恕我直言,它再简单不过了:如果您的测试是关于确保您的演示者将调用保存,请执行预期。如果您的测试是关于确保您的 Presenter 将在 Save 抛出时优雅地处理异常,请执行 Stub。
有关更多详细信息,请查看Hanselman 和 Osherove(单元测试艺术的作者)的播客