代码之家  ›  专栏  ›  技术社区  ›  Esteban Araya

我应该什么时候嘲笑?

  •  106
  • Esteban Araya  · 技术社区  · 17 年前

    我对模拟和假对象有基本的了解,但我不确定何时/何地使用模拟-特别是当它适用于此场景时 here .

    4 回复  |  直到 11 年前
        1
  •  102
  •   Drew Stephens    12 年前

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

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

    如果您的依赖是错误的,那么您的测试可能会受到影响,从而返回一个假阳性。例如,您可能会将一个意外的空值传递给依赖项,并且依赖项可能不会像文档中描述的那样将空值传递给它。您的测试没有像应该的那样遇到空参数异常,测试通过了。

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

    模拟代替了这种依赖。您可以设置对依赖对象调用的期望值,设置执行所需测试时它应该给您的确切返回值,和/或抛出什么异常,以便测试异常处理代码。通过这种方式,您可以轻松地测试有问题的单元。

    tl;dr:模拟单元测试涉及的每个依赖项。

        2
  •  134
  •   palacsint    11 年前

    模拟对象在需要时很有用 测试交互 在测试类和特定接口之间。

    例如,我们要测试这个方法 sendInvitations(MailServer mailServer) 电话 MailServer.createMessage() 就一次,也打电话 MailServer.sendMessage(m) 只调用一次,并且在 MailServer 接口。这是我们可以使用模拟对象的时候。

    使用模拟对象,而不是传递 MailServerImpl 或测试 TestMailServer ,我们可以通过 邮件服务器 接口。在我们通过模拟前 邮件服务器 ,我们对它进行“训练”,以便它知道什么方法调用预期值,以及返回什么值。最后,模拟对象断言,所有预期的方法都按预期调用。

    这在理论上听起来不错,但也有一些缺点。

    模拟缺点

    如果您已经有了一个模拟框架,那么您将倾向于使用模拟对象。 每一次 您需要将接口传递给测试中的类。这样你就完蛋了 即使没有必要也要测试交互 . 不幸的是,对交互进行不必要的(意外的)测试是不好的,因为然后您要测试特定需求是以特定的方式实现的,而不是实现产生了所需的结果。

    下面是一个伪代码示例。假设我们已经创建了一个 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 
        ....
    }
    

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

    在这样一个极端的例子中,很明显为什么后一个例子是错误的。当我们改变 肌切除器 第一个测试在确保我们仍然正确排序方面做了很大的工作,这就是测试的全部要点——它们允许我们安全地更改代码。另一方面,后者测试 总是 破坏,它是积极有害的;它妨碍重构。

    嘲讽

    模拟框架通常也允许不那么严格的使用,在这种情况下,我们不必精确地指定应该调用多少次方法以及需要什么参数;它们允许创建模拟对象,这些对象用作 stubs .

    假设我们有一个方法 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
    }
    

    在这个例子中,我们并不真正关心 PDF格式化程序 对象,所以我们只是训练它安静地接受任何调用,并为所有 sendInvitation() 这时正好打电话来。我们是怎么想出这个训练方法的清单的?我们只是运行测试并不断添加方法,直到测试通过。注意,我们训练了存根来响应一个方法,却不知道为什么需要调用它,我们只是简单地添加了测试抱怨的所有内容。我们很高兴,考试通过了。

    但是当我们改变的时候 sendInvitations() 或者其他的类 发送邀请() 使用,以创建更精致的PDF?我们的测试突然失败了,因为现在更多的方法 PDF格式化程序 我们没有训练我们的存根去期待他们。通常情况下,在这种情况下失败的不仅仅是一个测试,而是直接或间接使用 发送邀请() 方法。我们必须通过增加培训来修正所有这些测试。还要注意,我们不能删除不再需要的方法,因为我们不知道哪些方法是不需要的。同样,它阻碍了重构。

    另外,测试的可读性也受到了极大的影响,因为我们想写的代码很多,但是我们必须写;不是我们想写的代码。使用模拟对象的测试看起来非常复杂,通常很难阅读。这些测试应该帮助读者理解,如何使用测试下的类,因此它们应该简单明了。如果它们不可读,没有人会维护它们;事实上,删除它们比维护它们容易。

    如何解决这个问题?容易地:

    • 尽可能使用真实的类而不是模拟。使用真实 PdfFormatterImpl . 如果不可能,请更改实际类以使其成为可能。如果不能在测试中使用类,通常会指出类中的一些问题。解决问题是一个双赢的局面——你解决了课堂,你有一个更简单的测试。另一方面,不修复它和使用mocks是一种不赢的情况——您没有修复真正的类,并且您有更复杂的、不太可读的测试,这些测试阻碍了进一步的重构。
    • 尝试创建接口的简单测试实现,而不是在每个测试中模拟它,并在所有测试中使用这个测试类。创造 TestPdfFormatter 那没用。这样,您就可以对所有测试只更改一次,并且您的测试不会被冗长的设置混乱,您可以在这些设置中训练存根。

    总之,模拟对象有其用途,但如果不小心使用, 它们常常鼓励坏的实践、测试实现细节、阻碍重构、产生难以阅读和难以维护的测试。 .

    有关mock缺点的更多详细信息,请参见 Mock Objects: Shortcomings and Use Cases .

        3
  •  44
  •   Orion Edwards    11 年前

    经验法则:

    如果正在测试的函数需要一个复杂的对象作为参数,并且仅仅实例化这个对象(例如,如果它试图建立TCP连接),那么就需要使用模拟。

        4
  •  3
  •   palacsint    11 年前

    当您试图测试的代码单元中有一个依赖项时,您应该模拟一个对象,而这个依赖项需要“恰好如此”。

    例如,当您试图测试代码单元中的某些逻辑时,但是您需要从另一个对象中获得一些东西,并且从这个依赖项返回的内容可能会影响您尝试测试的内容—模拟该对象。

    可以找到关于这个主题的一个很好的播客 here