206

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

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

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

我在找:

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

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

4

6 回答 6

81

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

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

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

更新:

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

以我的经验,如果我改变一个类的实现,我经常不得不改变它的单元测试的实现来匹配。不过,通常情况下,我不必更改班级单元测试的清单。当然,除非更改的原因是存在我之前未能测试的条件。

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

于 2012-09-22T00:51:32.333 回答
65

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

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

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

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

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

于 2012-09-23T11:16:38.317 回答
31

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

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

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

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

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

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

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

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

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

于 2012-09-24T09:40:50.510 回答
8

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

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

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

于 2015-03-30T15:16:23.243 回答
0

正如一些人所说

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

关于您在重构时对破坏测试的担忧,这在使用模拟/存根/间谍时有些意料之中。我的意思是根据定义,而不是针对特定的实现,例如 Mockito。但是你可以这样想——如果你需要做一个重构来改变你的方法的工作方式,那么在 TDD 方法上做是个好主意,这意味着你可以改变你的测试来定义新的行为(这将使测试失败),然后进行更改并再次通过测试。

于 2017-03-20T02:55:51.180 回答
0

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

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

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

于 2019-10-29T14:15:54.987 回答