何时使用Mockito.verify()?


201

我为3个目的编写jUnit测试用例:

  1. 为确保我的代码在所有(或大部分)输入组合/值下满足所有必需的功能。
  2. 为了确保可以更改实现,并依靠JUnit测试用例告诉我,我的所有功能仍然令人满意。
  3. 作为所有用例的文档,我的代码将处理这些代码,并充当重构的规范-如果需要重写代码。(重构代码,如果我的jUnit测试失败-您可能错过了一些用例)。

我不明白为什么或何时Mockito.verify()使用。当我看到verify()被调用时,它告诉我我的jUnit正在意识到实现。(即使我的功能不受影响,更改我的实现也会破坏jUnits)。

我在找:

  1. 正确使用的准则应该是什么Mockito.verify()

  2. jUnits知道或紧密关联被测类的实现在本质上是正确的吗?


1
由于您暴露的相同原因,我尝试尽量不使用verify()(我不希望我的单元测试了解实现),但是在某些情况下,我别无选择-残缺的无效方法。一般来说,由于它们不返回任何内容,因此不会对您的“实际”输出有所帮助;但是,您仍然需要知道它被调用了。但是我同意你的观点,使用验证来验证执行流程是没有意义的。
莱格纳

Answers:


78

如果类A的约定包括它调用类型为C的对象的方法B的事实,则应通过模拟类型C并验证是否已调用方法B来进行测试。

这意味着类A的协定具有足够的细节,足以讨论类型C(可能是接口或类)。因此,是的,我们所讨论的规范水平不仅限于“系统要求”,而且还以某种方式描述了实现。

这对于单元测试是正常的。在进行单元测试时,您要确保每个单元都在做“正确的事情”,并且通常将包括其与其他单元的交互。这里的“单位”可能表示类,或应用程序的更大子集。

更新:

我觉得这不仅适用于验证,还适用于存根。从某种意义上讲,一旦您对协作类的方法进行存根,您的单元测试就变得依赖于实现。单元测试的本质就是这样。由于Mockito既涉及存根又涉及验证,因此,您完全使用Mockito的事实意味着您将遇到这种依赖性。

以我的经验,如果我更改一个类的实现,那么我通常不得不更改其单元测试的实现以进行匹配。通常情况下,虽然,我不会改变什么单元测试有库存为类; 除非当然,否则更改的原因是存在我之前无法测试的条件。

这就是单元测试的目的。不受协作类使用方式依赖的测试实际上是子系统测试或集成测试。当然,它们也经常用JUnit编写,并且经常涉及到模拟的使用。在我看来,“ JUnit”是一个糟糕的名字,因为它使我们能够进行所有不同类型的测试。


8
谢谢大卫。在扫描了一些代码集之后,这似乎是一种常见的做法-但是对我而言,这违背了创建单元测试的目的,而只是增加了以很小的价值维护它们的开销。我确实理解为什么需要模拟,以及为什么需要设置执行测试的依赖关系。但我认为,验证方法dependencyA.XYZ()是否已执行会使测试非常脆弱。
罗素

@Russell即使“ type C”是围绕库或应用程序某些子系统的包装器的接口?
达伍德·伊本·卡里姆

1
我不会说确保某些子系统或服务被调用完全是无用的,只是在它周围应该有一些准则(制定它们就是我想要做的)。例如:(我可能过于简单化了)说,我在我的代码中使用StrUtil.equals(),并决定在实现中切换到StrUtil.equalsIgnoreCase()。如果jUnit具有verify(StrUtil.equals ),尽管实现准确,但我的测试可能会失败。尽管此IMO验证调用是针对库/子系统的,但它是不好的做法。另一方面,使用验证来确保对closeDbConn的调用可能是有效的用例。
罗素

1
我了解您,并完全同意您的意见。但是我也觉得编写您描述的指南可能会扩展为编写整个TDD或BDD教科书。以您的示例为例,调用equals()equalsIgnoreCase()绝不会是类要求中指定的内容,因此它本身就不会有单元测试。但是,“完成时关闭数据库连接”(无论从实现的角度而言,这都意味着)是类的要求,即使这不是“业务要求”。对我来说,这
取决于

...在其业务需求中表示的类,以及对该类进行单元测试的一组测试方法。在有关TDD或BDD的任何书籍中,定义这种关系将是一个重要主题。尽管Mockito团队中的某人可以为自己的Wiki撰写有关该主题的文章,但我不认为它与许多其他可用文献有何不同。如果您知道它可能有什么不同,请告诉我,也许我们可以一起努力。
达伍德·伊本·卡里姆

60

David的答案当然是正确的,但是并没有完全解释您为什么要这么做。

基本上,在进行单元测试时,您是在独立测试功能单元。您测试输入是否产生预期的输出。有时,您还必须测试副作用。简而言之,验证允许您执行此操作。

例如,您有一些应该使用DAO存储事物的业务逻辑。您可以使用实例化DAO的集成测试来执行此操作,将其绑定到业务逻辑,然后在数据库中四处查看是否存储了预期的东西。那不再是单元测试了。

或者,您可以模拟DAO并验证它是否以您期望的方式被调用。使用mockito,您可以验证是否调用了某个东西,调用了它的频率,甚至可以在参数上使用匹配器以确保以特定方式调用它。

像这样的单元测试的另一面确实是您将测试与实现联系在一起,这使得重构更加困难。另一方面,良好的设计气味是正确行使其所需的代码量。如果您的测试需要很长的时间,则可能是设计有问题。因此,具有很多副作用/复杂交互作用且需要进行测试的代码可能不是一件好事。


29

这是个好问题!我认为其根本原因是以下原因,我们不仅将JUnit用于单元测试。因此,应该对问题进行分解:

  • 我是否应该在集成测试(或任何其他高于单元的测试)中使用Mockito.verify()?
  • 我应该在黑盒单元测试使用Mockito.verify()吗?
  • 我应该在白盒单元测试使用Mockito.verify()吗?

因此,如果我们忽略单元测试以上的问题,可以改写以下问题:“ 白盒单元测试与Mockito.verify()结合使用,可以在单元测试和我的can实施之间建立很好的结合,我可以将一些“灰盒“单元测试以及对此应该使用的经验法则 ”。

现在,让我们逐步完成所有这些步骤。

*-我是否应该在集成测试(或任何其他高于单元测试的测试)中使用Mockito.verify()?*我认为答案显然不是,此外,您不应为此使用模拟。您的测试应尽可能接近实际应用。您正在测试完整的用例,而不是应用程序的独立部分。

* 黑盒白盒单元测试 *如果您使用黑盒方法,您实际上在做什么,那么您将提供(所有等效类)输入,状态并测试您将收到预期的输出。在这种方法中,通常使用模拟是有道理的(您只是模仿它们在做正确的事;您不想测试它们),但是调用Mockito.verify()是多余的。

如果您使用的是白盒方法,那么您实际上正在做什么,那么您正在测试单元的行为。在这种方法中,调用Mockito.verify()是必不可少的,您应该验证单元的行为符合预期。

灰盒测试 的经验法则白盒测试的问题是它会产生很高的耦合度。一种可能的解决方案是进行灰盒测试,而不是白盒测试。这是黑白盒测试的组合。就像在白盒测试中一样,您实际上是在测试单元的行为,但总的来说,在可能的情况下,使其与实现无关。如果可能的话,您将像在黑盒中那样进行检查,只是断言输出是您期望的结果。因此,您的问题的实质是何时有可能。

这真的很难。我没有一个很好的例子,但是我可以举几个例子。在上面用equals()vs equalsIgnoreCase()提到的情况下,您不应该调用Mockito.verify(),只需声明输出即可。如果您无法执行此操作,请将代码分解为较小的单元,直到可以执行为止。另一方面,假设您有一些@Service,并且正在编写@ Web-Service,它实际上是对@Service的包装-它将所有调用委派给@Service(并进行一些额外的错误处理)。在这种情况下,调用Mockito.verify()是必不可少的,您不应复制对@Serive所做的所有检查,以验证您使用正确的参数列表来调用@Service就足够了。


灰盒测试是一个陷阱。我倾向于将其限制为DAO之类的东西。由于大量的灰箱测试,几乎完全缺少单元测试以及太多的黑箱测试以弥补人们对灰箱测试应该进行的测试缺乏信任,我参与了一些构建速度极慢的项目。
Jilles van Gurp

对我来说,这是最佳的答案,因为它回答了在各种情况下何时使用Mockito.when()。做得好。
Michiel Leegwater

8

我必须说,从经典方法的角度来看,您绝对是正确的:

  • 如果您首先创建(或更改)应用程序的业务逻辑,然后使用(采用)测试Test-Last方法覆盖它,那么让测试知道有关软件工作方式的信息将非常痛苦且危险,检查输入和输出。
  • 如果您正在实践“ 测试驱动”方法,那么您的测试就是第一个要编写,要更改的测试,并且要反映软件功能的用例实现取决于测试。有时这意味着您希望以某种特定的方式来实现您的软件,例如,依靠某些其他组件的方法,或者甚至调用特定的时间。那就是Mockito.verify()派上用场的地方!

重要的是要记住,没有通用工具。软件的类型,大小,公司目标和市场状况,团队技能以及许多其他因素会影响在特定情况下使用哪种方法的决策。


0

正如某些人所说

  1. 有时您没有可以声明的直接输出
  2. 有时,您只需要确认您所测试的方法正在向其协作者发送正确的间接输出(您正在嘲笑)。

关于您在重构时会破坏测试的担忧,使用模拟/存根/间谍可能会有些许预期。我的意思是按照定义,而不是针对诸如Mockito的特定实现。但是你可以这样想-如果你需要做的是将创建的道路上重大变化的重构你的方法的作品,这是一个好主意,做一个TDD的方式,这意味着你可以改变你的测试首先来定义新行为(将导致测试失败),然后进行更改并再次通过测试。


0

在大多数情况下,当人们不喜欢使用Mockito.verify时,这是因为它用于验证受测单元正在执行的所有操作,这意味着您需要对测试进行任何更改,以适应其变化。但是,我认为这不是问题。如果您希望能够更改方法的功能而无需更改其测试,那基本上意味着您想要编写不测试方法所做的所有事情的测试,因为您不希望它测试更改。那是错误的思维方式。

真正的问题是,如果您可以修改方法所做的事情,并且应该完全覆盖功能的单元测试不会失败。这意味着无论您进行更改的意图是什么,测试结果都不会涵盖更改的结果。

因此,我更喜欢尽可能地模拟:还模拟您的数据对象。这样做时,您不仅可以使用验证来检查是否调用了其他类的正确方法,而且还可以通过这些数据对象的正确方法来收集正在传递的数据。为了使其完整,您应该测试调用发生的顺序。示例:如果您修改db实体对象,然后使用存储库将其保存,则不足以验证是否使用正确的数据调用了对象的setter并调用了存储库的save方法。如果以错误的顺序调用它们,则您的方法仍未执行应做的事情。因此,我不使用Mockito.verify,而是创建一个包含所有模拟的inOrder对象,而改用inOrder.verify。如果您想使其完整,还应该致电Mockito。最后验证verifyMoreMoreInteractions并将其传递给所有模拟对象。否则,有人可以在不测试的情况下添加新的功能/行为,这意味着您的覆盖率统计信息可能达到100%之后,您仍然在堆积未经声明或验证的代码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.