Est*_*aya 122 language-agnostic unit-testing mocking
我有模仿和假冒对象的一个基本的了解,但我不知道我有一个关于何时/何用嘲弄的感觉-特别是因为它也适用于这种情况在这里.
Jan*_*tis 152
当您想要测试被测试类和特定接口之间的交互时,模拟对象很有用.
例如,我们想要测试该方法只sendInvitations(MailServer mailServer)调用MailServer.createMessage()一次,并且只调用MailServer.sendMessage(m)一次,并且在MailServer接口上不调用其他方法.这是我们可以使用模拟对象的时候.
使用模拟对象,我们可以传递接口的模拟实现,而不是传递真实MailServerImpl或测试.在我们传递模拟之前,我们"训练"它,以便它知道调用期望的方法以及返回的返回值.最后,模拟对象断言,所有预期的方法都按预期调用.TestMailServerMailServerMailServer
这在理论上听起来不错,但也存在一些缺点.
如果你有一个模拟框架,那么每次你需要将一个接口传递给测试中的类时,你很想使用模拟对象.这样,即使没有必要,您最终也会测试交互.不幸的是,对交互进行不必要的(偶然)测试是不好的,因为那时您正在测试特定需求是以特定方式实现的,而不是实现产生了所需的结果.
这是伪代码的一个例子.假设我们已经创建了一个MySorter类,我们想测试它:
// the correct way of testing
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert testList equals [1, 2, 3, 7, 8]
}
// incorrect, testing implementation
testSort() {
testList = [1, 7, 3, 8, 2]
MySorter.sort(testList)
assert that compare(1, 2) was called once
assert that compare(1, 3) was not called
assert that compare(2, 3) was called once
....
}
Run Code Online (Sandbox Code Playgroud)
(在这个例子中,我们假设它不是我们想要测试的特定排序算法,例如快速排序;在这种情况下,后一个测试实际上是有效的.)
在这样一个极端的例子中,显而易见的是后一个例子是错误的.当我们改变实现时MySorter,第一个测试确保我们仍然正确排序,这是测试的全部要点 - 它们允许我们安全地更改代码.另一方面,后一种测试总是破坏而且它是有害的; 它阻碍了重构.
模拟框架通常也允许不太严格的使用,我们不必确切地指定应该调用多少次方法以及期望什么参数; 它们允许创建用作存根的模拟对象.
假设我们有一个sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)我们想要测试的方法.该PdfFormatter对象可用于创建邀请.这是测试:
testInvitations() {
// train as stub
pdfFormatter = create mock of PdfFormatter
let pdfFormatter.getCanvasWidth() returns 100
let pdfFormatter.getCanvasHeight() returns 300
let pdfFormatter.addText(x, y, text) returns true
let pdfFormatter.drawLine(line) does nothing
// train as mock
mailServer = create mock of MailServer
expect mailServer.sendMail() called exactly once
// do the test
sendInvitations(pdfFormatter, mailServer)
assert that all pdfFormatter expectations are met
assert that all mailServer expectations are met
}
Run Code Online (Sandbox Code Playgroud)
在这个例子中,我们并不真正关心PdfFormatter对象,所以我们只是训练它静静地接受任何调用,并为此时sendInvitation()恰好调用的所有方法返回一些合理的固定返回值.我们怎么想出这个训练方法列表呢?我们只是运行测试并继续添加方法,直到测试通过.请注意,我们训练存根以响应方法而不知道为什么需要调用它,我们只是添加了测试所抱怨的所有内容.我们很高兴,测试通过.
但是,当我们改变sendInvitations()或者其他一些sendInvitations()使用的类来创建更多花哨的pdf时会发生什么呢?我们的测试突然失败了,因为现在PdfFormatter调用了更多的方法,我们没有训练我们的存根来预期它们.通常,在这种情况下,不仅一个测试失败,而是直接或间接使用该sendInvitations()方法的任何测试.我们必须通过添加更多培训来修复所有这些测试.另请注意,我们无法删除不再需要的方法,因为我们不知道哪些方法不需要.同样,它阻碍了重构.
此外,测试的可读性非常糟糕,那里有很多代码我们没有写,因为我们想要,但因为我们必须; 我们不想在那里使用那些代码.使用模拟对象的测试看起来非常复杂,通常难以阅读.测试应该有助于读者理解应该如何使用测试中的类,因此它们应该简单明了.如果它们不可读,没有人会维护它们; 实际上,删除它们比维护它们更容易.
如何解决?容易:
PdfFormatterImpl.如果不可能,请更改实际类以使其成为可能.在测试中无法使用类通常会指出类的一些问题.解决问题是一个双赢的局面 - 你修复了课程,你有一个更简单的测试.另一方面,不修复它并使用模拟是一种不赢的情况 - 你没有修复真正的类,你有更复杂,更不易读的测试,阻碍了进一步的重构.TestPdfFormatter什么都不做.这样,您可以为所有测试更改一次,并且您的测试不会在您训练存根的冗长设置中混乱.总而言之,模拟对象有其用途,但如果不小心使用,它们通常会鼓励不良做法,测试实现细节,阻碍重构并产生难以阅读和难以维护的测试.
有关模拟缺点的更多细节,请参阅模拟对象:缺点和用例.
小智 116
单元测试应该通过单个方法测试单个代码路径.当方法的执行从该方法之外传递到另一个对象,然后再返回时,您就有了依赖关系.
当您使用实际依赖项测试该代码路径时,您不是单元测试; 你是集成测试.虽然这是好的和必要的,但它不是单元测试.
如果您的依赖项有问题,您的测试可能会以某种方式受到影响,从而返回误报.例如,您可以将依赖项传递给意外的null,并且依赖项可能不会因为文档记录而抛出null.您的测试不会发现它应该具有的null参数异常,并且测试通过.
此外,您可能会发现很难(如果不是不可能的话)可靠地让依赖对象在测试期间准确返回您想要的内容.这还包括在测试中抛出预期的异常.
模拟取代了该依赖项.您可以设置对依赖对象的调用的期望,设置它应该为您执行所需的测试提供的确切返回值,和/或要抛出的异常,以便您可以测试异常处理代码.通过这种方式,您可以轻松地测试有问题的装置.
TL; DR:模拟你的单元测试所涉及的每一个依赖.
| 归档时间: |
|
| 查看次数: |
41212 次 |
| 最近记录: |