当前位置:  开发笔记 > 人工智能 > 正文

什么时候应该嘲笑?

如何解决《什么时候应该嘲笑?》经验,为你挑选了3个好方法。

我有模仿和假冒对象的一个基本的了解,但我不知道我有一个关于何时/何用嘲弄的感觉-特别是因为它也适用于这种情况在这里.



1> Jan Soltis..:

当您想要测试被测试类和特定接口之间的交互时,模拟对象很有用.

例如,我们想要测试该方法只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 
    ....
}

(在这个例子中,我们假设它不是我们想要测试的特定排序算法,例如快速排序;在这种情况下,后一个测试实际上是有效的.)

在这样一个极端的例子中,显而易见的是后一个例子是错误的.当我们改变实现时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什么都不做.这样,您可以为所有测试更改一次,并且您的测试不会在您训练存根的冗长设置中混乱.

总而言之,模拟对象有其用途,但如果不小心使用,它们通常会鼓励不良做法,测试实现细节,阻碍重构并产生难以阅读和难以维护的测试.

有关模拟缺点的更多细节,请参阅模拟对象:缺点和用例.


"在测试中无法使用课程通常会导致课堂上出现一些问题." 如果类是服务(例如,访问数据库或代理到Web服务),则应将其视为外部依赖性并模拟/存根

2> 小智..:

单元测试应该通过单个方法测试单个代码路径.当方法的执行从该方法之外传递到另一个对象,然后再返回时,您就有了依赖关系.

当您使用实际依赖项测试该代码路径时,您不是单元测试; 你是集成测试.虽然这是好的和必要的,但它不是单元测试.

如果您的依赖项有问题,您的测试可能会以某种方式受到影响,从而返回误报.例如,您可以将依赖项传递给意外的null,并且依赖项可能不会因为文档记录而抛出null.您的测试不会发现它应该具有的null参数异常,并且测试通过.

此外,您可能会发现很难(如果不是不可能的话)可靠地让依赖对象在测试期间准确返回您想要的内容.这还包括在测试中抛出预期的异常.

模拟取代了该依赖项.您可以设置对依赖对象的调用的期望,设置它应该为您执行所需的测试提供的确切返回值,和/或要抛出的异常,以便您可以测试异常处理代码.通过这种方式,您可以轻松地测试有问题的装置.

TL; DR:模拟你的单元测试所涉及的每一个依赖.


这个答案过于激进了.单元测试可以并且应该运行多于一种方法,只要它们都属于同一个内聚单元.否则将需要太多的嘲弄/伪造,导致复杂和脆弱的测试.只有那些不属于被测单元的依赖项才能通过模拟替换.
这个答案也太乐观了.如果它包含@ Jan的模拟对象的缺点会更好.
嘲笑你的单元测试接触的每一个依赖.这解释了一切.

3> Orion Edward..:

经验法则:

如果您正在测试的函数需要一个复杂的对象作为参数,那么简单地实例化该对象(例如,如果它尝试建立TCP连接)将是一件痛苦的事情,请使用模拟.

推荐阅读
手机用户2402852387
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有