繁体   English   中英

什么时候使用 Mockito.verify()?

[英]When to use Mockito.verify()?

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

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

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

我在找:

  1. 适当使用Mockito.verify()的准则应该是什么?

  2. jUnits 知道或紧密耦合到被测类的实现是否从根本上是正确的?

如果类 A 的契约包括它调用类型 C 对象的方法 B 的事实,那么您应该通过创建类型 C 的模拟来测试这一点,并验证方法 B 已被调用。

这意味着类 A 的契约有足够的细节来讨论类型 C(可能是接口或类)。 所以,是的,我们谈论的规范级别不仅仅是“系统要求”,而且在某种程度上描述了实现。

这对于单元测试来说是正常的。 当您进行单元测试时,您希望确保每个单元都在做“正确的事情”,这通常包括它与其他单元的交互。 此处的“单元”可能是指类或应用程序的更大子集。

更新:

我觉得这不仅适用于验证,也适用于存根。 一旦您存根合作者类的方法,您的单元测试就在某种意义上变得依赖于实现。 单元测试的本质就是这样。 由于 Mockito 与验证一样重要,因此您完全使用 Mockito 的事实意味着您将遇到这种依赖关系。

根据我的经验,如果我改变一个类的实现,我经常不得不改变它的单元测试的实现来匹配。 通常情况下,虽然,我不会改变什么单元测试该类的库存; 当然,除非更改的原因是存在我之前未能测试的条件。

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

大卫的回答当然是正确的,但并没有完全解释为什么你会想要这个。

基本上,在进行单元测试时,您是在单独测试一个功能单元。 您测试输入是否产生预期的输出。 有时,您还必须测试副作用。 简而言之,verify 允许您这样做。

例如,您有一些业务逻辑应该使用 DAO 存储事物。 您可以使用集成测试来实现这一点,该测试实例化 DAO,将其连接到业务逻辑,然后在数据库中查看是否存储了预期的内容。 那不再是单元测试了。

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

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

这是个好问题! 我认为它的根本原因如下,我们不仅使用 JUnit 进行单元测试。 所以这个问题应该拆分:

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

所以如果我们忽略高于单元的测试,这个问题可以改写为“使用白盒单元测试和 Mockito.verify() 在单元测试和我的可能实现之间创造了很好的结合,我可以做一些“灰盒“单元测试以及我应该使用什么经验法则”。

现在,让我们一步一步地完成所有这些。

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

*黑盒vs白盒单元测试* 如果你使用黑盒方法你真正在做什么,你提供(所有等价类)输入、状态和测试,你将收到预期的输出。 在这种方法中,一般使用模拟是合理的(你只是模仿他们在做正确的事情;你不想测试它们),但调用 Mockito.verify() 是多余的。

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

灰盒测试的经验法则 白盒测试的问题在于它会产生高耦合。 一种可能的解决方案是进行灰盒测试,而不是白盒测试。 这是一种黑白盒测试的组合。 您实际上是在测试单元的行为,就像在白盒测试中一样,但一般而言,您在可能的情况下使其与实现无关。 如果可能,您将像在黑盒情况下一样进行检查,只需断言输出是您所期望的。 所以,你的问题的本质是什么时候有可能。

这真的很难。 我没有很好的例子,但我可以给你举个例子。 在上面提到的 equals() 与 equalsIgnoreCase() 的情况下,您不应调用 Mockito.verify(),只需断言输出即可。 如果你做不到,把你的代码分解成更小的单元,直到你能做到。 另一方面,假设您有一些 @Service 并且您正在编写本质上是 @Service 包装器的 @Web-Service - 它会将所有调用委托给 @Service (并进行一些额外的错误处理)。 在这种情况下,调用 Mockito.verify() 是必不可少的,您不应该重复您为 @Serive 所做的所有检查,验证您正在使用正确的参数列表调用 @Service 就足够了。

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

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

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

正如一些人所说

  1. 有时您没有可以断言的直接输出
  2. 有时您只需要确认您的测试方法是否将正确的间接输出发送给其合作者(您正在嘲笑)。

关于在重构时破坏测试的担忧,在使用模拟/存根/间谍时在某种程度上是预料之中的。 我的意思是根据定义而不是关于特定的实现,例如 Mockito。 但是你可以这样想-如果你需要做的是将创建的道路上重大变化的重构你的方法的作品,这是一个好主意,做一个TDD的方式,这意味着你可以改变你的测试定义新行为(这将使测试失败),然后进行更改并再次通过测试。

在大多数情况下,当人们不喜欢使用 Mockito.verify 时,这是因为它用于验证被测试单元所做的一切,这意味着如果有任何变化,您将需要调整测试。 但是,我认为这不是问题。 如果您希望能够在不需要更改测试的情况下更改方法的功能,那基本上意味着您要编写不测试您的方法所做的一切的测试,因为您不希望它测试您的更改. 这是错误的思维方式。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM