我应该什么时候嘲笑?

我对假冒和伪造物品有一个基本的了解,但是我不确定自己有什么时间/在哪里使用嘲弄的感觉 – 特别是因为它适用于这种场景。

unit testing应该通过一种方法来testing单个代码path。 当一个方法的执行超出该方法的范围,进入另一个对象,然后再返回时,就会产生依赖关系。

当你用实际的依赖testing代码path时,你不是unit testing; 你是集成testing。 虽然这是好的和必要的,但不是unit testing。

如果你的依赖是bug,你的testing可能会受到影响,返回一个误报。 例如,您可能会传递一个意外的空依赖项,依赖项可能不会抛出null,因为它被logging下来。 你的testing不会像它应该有的那样产生一个空的参数exception,并且testing通过。

而且,你可能会发现,即使不是不可能,也很难确定依赖对象在testing期间返回到你想要的结果。 这也包括在testing中抛出预期的exception。

模拟取代了这种依赖。 您可以设置对依赖对象调用的期望值,设置它应该给予的确切返回值以执行所需的testing,以及/或抛出哪些exception以便testingexception处理代码。 通过这种方式,您可以轻松testing相关单元。

TL; DR:模拟你的unit testing涉及的每一个依赖。

模拟对象在您要testing被testing类和特定接口之间的交互时非常有用。

例如,我们想testing一下sendInvitations(MailServer mailServer)调用MailServer.createMessage()一次,并且只调用一次MailServer.sendMessage(m) ,并且在MailServer接口上不调用其他方法。 这是当我们可以使用模拟对象。

使用模拟对象,而不是传递一个真正的MailServerImpl ,或一个testingTestMailServer ,我们可以传递一个MailServer接口的模拟实现。 在我们传递一个模拟MailServer之前,我们“训练”它,以便它知道什么方法调用预期和返回值。 最后,模拟对象断言,所有预期的方法都按预期调用。

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

模拟缺点

如果你有一个模拟框架,你每次需要传递一个接口到testing下的类 ,都试图使用模拟对象。 这样, 即使没有必要,也可以通过这种方式来testing交互 。 不幸的是,对交互的不必要的(意外的)testing是不好的,因为那么你正在testing一个特定的需求是以一种特定的方式来实现的,而不是实现产生了所需的结果。

这里是一个伪代码的例子。 假设我们已经创build了一个MySorter类,我们想testing它:

 // 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 .... } 

(在这个例子中,我们假设它不是一个特定的sortingalgorithm,比如我们想要testing的快速sortingalgorithm;在这种情况下,后面的testing实际上是有效的)。

在这样一个极端的例子中,为什么后面的例子是错误的。 当我们改变MySorter的实现时,第一个testing做了很好的工作,确保我们仍然可以正确sorting,这是testing的重点 – 它允许我们安全地更改代码。 另一方面,后一种testing总是被打破,而且是有害的。 它阻碍了重构。

嘲笑作为存根(stub)

模拟框架通常也允许不那么严格的用法,我们不必确切地指定应该调用多less次方法,以及期望什么参数; 他们允许创build用作存根的模拟对象。

假设我们有一个我们想要testing的sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)方法。 PdfFormatter对象可以用来创build邀请。 这是testing:

 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()在此时调用的所有方法的一些合理的返回值。 我们是怎么想出这种训练方法的呢? 我们只是运行testing,并不断添加方法,直到testing通过。 注意,我们训练存根来响应一个方法,而不需要知道为什么需要调用它,我们只是添加了testing所抱怨的一切。 我们很高兴,testing通过。

但是,当我们改变sendInvitations()sendInvitations()使用的其他类时,会发生什么情况来创build更多花式pdf? 我们的testing突然失败了,因为现在调用了更多的PdfFormatter方法,并且我们没有训练我们的存根期望它们。 通常,这不仅仅是一个在这种情况下失败的testing,而是直接或间接使用sendInvitations()方法的任何testing。 我们必须通过添加更多的培训来修复所有这些testing。 另外请注意,我们不能删除不再需要的方法,因为我们不知道哪些是不需要的。 再次,它阻碍了重构。

另外,testing的可读性也非常糟糕,有很多代码是我们没有写的,因为我们想要,但是因为我们必须; 这不是我们想要那里的代码。 使用模拟对象的testing看起来非常复杂,往往难以阅读。 这些testing应该帮助读者理解应该如何使用testing的类,因此它们应该是简单明了的。 如果它们不可读,则没有人会维护它们; 事实上,删除它们比维护它们更容易。

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

  • 尝试使用真正的类,而不是尽可能的模仿。 使用真正的PdfFormatterImpl 。 如果这是不可能的,改变真正的课程,使之成为可能。 在testing中不能使用class级通常会指出class级中的一些问题。 解决问题是一个双赢的局面 – 你固定课堂,你有一个更简单的testing。 另一方面,不修复和使用模拟是一个双赢的情况 – 你没有修复真正的类,你有更复杂,更不可读的testing,阻碍进一步的重构。
  • 尝试创build一个接口的简单testing实现,而不是在每个testing中模拟它,并在所有的testing中使用这个testing类。 创build不执行任何操作的TestPdfFormatter 。 这样,您可以对所有testing进行一次更改,并且您的testing不会在训练存根的冗长设置中混乱。

总而言之,模拟对象有它们的用途,但是如果不仔细使用, 它们往往鼓励不好的实践,testing实现细节,阻碍重构,产生难以阅读和难以维护的testing

有关嘲讽缺点的更多详细信息,请参见模仿对象:缺点和用例 。

经验法则:

如果你正在testing的函数需要一个复杂的对象作为参数,并且简单地实例化这个对象(例如,如果它尝试build立一个TCP连接)会是一个痛苦,那么就使用一个模拟器。

你应该嘲笑一个对象,当你试图testing的代码单元中有一个依赖项时,就需要“这样”。

例如,当你试图在你的代码单元中testing一些逻辑,但是你需要从另一个对象中获取某些东西,从这个依赖返回的东西可能会影响你正在testing的东西 – 嘲笑那个对象。

关于这个话题的伟大的播客可以在这里find