我有模仿和假冒对象的一个基本的了解,但我不知道我有一个关于何时/何用嘲弄的感觉-特别是因为它也适用于这种情况在这里.
当您想要测试被测试类和特定接口之间的交互时,模拟对象很有用.
例如,我们想要测试该方法只sendInvitations(MailServer mailServer)
调用MailServer.createMessage()
一次,并且只调用MailServer.sendMessage(m)
一次,并且在MailServer
接口上不调用其他方法.这是我们可以使用模拟对象的时候.
使用模拟对象,我们可以传递接口的模拟实现,而不是传递真实MailServerImpl
或测试.在我们传递模拟之前,我们"训练"它,以便它知道调用期望的方法以及返回的返回值.最后,模拟对象断言,所有预期的方法都按预期调用.TestMailServer
MailServer
MailServer
这在理论上听起来不错,但也存在一些缺点.
如果你有一个模拟框架,那么每次你需要将一个接口传递给测试中的类时,你很想使用模拟对象.这样,即使没有必要,您最终也会测试交互.不幸的是,对交互进行不必要的(偶然)测试是不好的,因为那时您正在测试特定需求是以特定方式实现的,而不是实现产生了所需的结果.
这是伪代码的一个例子.假设我们已经创建了一个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 .... }
(在这个例子中,我们假设它不是我们想要测试的特定排序算法,例如快速排序;在这种情况下,后一个测试实际上是有效的.)
在这样一个极端的例子中,显而易见的是后一个例子是错误的.当我们改变实现时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 }
在这个例子中,我们并不真正关心PdfFormatter
对象,所以我们只是训练它静静地接受任何调用,并为此时sendInvitation()
恰好调用的所有方法返回一些合理的固定返回值.我们怎么想出这个训练方法列表呢?我们只是运行测试并继续添加方法,直到测试通过.请注意,我们训练存根以响应方法而不知道为什么需要调用它,我们只是添加了测试所抱怨的所有内容.我们很高兴,测试通过.
但是,当我们改变sendInvitations()
或者其他一些sendInvitations()
使用的类来创建更多花哨的pdf时会发生什么呢?我们的测试突然失败了,因为现在PdfFormatter
调用了更多的方法,我们没有训练我们的存根来预期它们.通常,在这种情况下,不仅一个测试失败,而是直接或间接使用该sendInvitations()
方法的任何测试.我们必须通过添加更多培训来修复所有这些测试.另请注意,我们无法删除不再需要的方法,因为我们不知道哪些方法不需要.同样,它阻碍了重构.
此外,测试的可读性非常糟糕,那里有很多代码我们没有写,因为我们想要,但因为我们必须; 我们不想在那里使用那些代码.使用模拟对象的测试看起来非常复杂,通常难以阅读.测试应该有助于读者理解应该如何使用测试中的类,因此它们应该简单明了.如果它们不可读,没有人会维护它们; 实际上,删除它们比维护它们更容易.
如何解决?容易:
尽可能尝试使用真实类而不是模拟.使用真实PdfFormatterImpl
.如果不可能,请更改实际类以使其成为可能.在测试中无法使用类通常会指出类的一些问题.解决问题是一个双赢的局面 - 你修复了课程,你有一个更简单的测试.另一方面,不修复它并使用模拟是一种不赢的情况 - 你没有修复真正的类,你有更复杂,更不易读的测试,阻碍了进一步的重构.
尝试创建接口的简单测试实现,而不是在每个测试中模拟它,并在所有测试中使用此测试类.创造TestPdfFormatter
什么都不做.这样,您可以为所有测试更改一次,并且您的测试不会在您训练存根的冗长设置中混乱.
总而言之,模拟对象有其用途,但如果不小心使用,它们通常会鼓励不良做法,测试实现细节,阻碍重构并产生难以阅读和难以维护的测试.
有关模拟缺点的更多细节,请参阅模拟对象:缺点和用例.
单元测试应该通过单个方法测试单个代码路径.当方法的执行从该方法之外传递到另一个对象,然后再返回时,您就有了依赖关系.
当您使用实际依赖项测试该代码路径时,您不是单元测试; 你是集成测试.虽然这是好的和必要的,但它不是单元测试.
如果您的依赖项有问题,您的测试可能会以某种方式受到影响,从而返回误报.例如,您可以将依赖项传递给意外的null,并且依赖项可能不会因为文档记录而抛出null.您的测试不会发现它应该具有的null参数异常,并且测试通过.
此外,您可能会发现很难(如果不是不可能的话)可靠地让依赖对象在测试期间准确返回您想要的内容.这还包括在测试中抛出预期的异常.
模拟取代了该依赖项.您可以设置对依赖对象的调用的期望,设置它应该为您执行所需的测试提供的确切返回值,和/或要抛出的异常,以便您可以测试异常处理代码.通过这种方式,您可以轻松地测试有问题的装置.
TL; DR:模拟你的单元测试所涉及的每一个依赖.
经验法则:
如果您正在测试的函数需要一个复杂的对象作为参数,那么简单地实例化该对象(例如,如果它尝试建立TCP连接)将是一件痛苦的事情,请使用模拟.