何时使用Mockito.verify()?

我写jUnit测试用例有三个目的:

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

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

我在寻找:

  1. 正确使用Mockito.verify()的指导原则应该是什么?

  2. junit意识到或紧密耦合到被测类的实现,这从根本上是正确的吗?

61775 次浏览

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

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

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

更新:

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

根据我的经验,如果我改变了一个类的实现,我经常不得不改变它的单元测试的实现来匹配。不过,通常情况下,我不需要改变单元测试的目录为类;当然,除非更改的原因是我之前未能测试的条件的存在。

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

David的回答当然是正确的,但并没有很好地解释为什么你想要这样做。

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

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

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

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

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

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

因此,如果我们忽略高于单元的测试,这个问题可以重新表述为“使用白盒单元测试与Mockito.verify()创建单元测试和我可以实现之间的伟大的一对,我可以做一些“灰盒”单元测试和什么规则的经验,我应该为此使用”。

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

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

*< em >黑盒 vs 白盒单元测试* 如果你使用黑盒方法,你真正要做的是,你提供(所有等价类)输入,一个状态和测试,你将收到预期的输出。在这种方法中,使用mock通常是证明(你只是模仿它们正在做正确的事情;你不想测试它们),但是调用Mockito.verify()是多余的

如果你使用白盒方法,你真正在做的是测试你单元的行为。在这种方法中,调用Mockito.verify()是必要的,您应该验证您的单元的行为是否如您所期望的那样。

灰盒测试的经验法则 白盒测试的问题在于它会产生高耦合。一个可能的解决方案是进行灰盒测试,而不是白盒测试。这是一种黑白盒测试的结合。你实际上是在测试你的单元的行为,就像在白盒测试中一样,但通常你让它成为实现不可知的在可能的情况下。在可能的情况下,你会像在黑盒情况下那样检查,只是断言输出是你期望的结果。所以,你问题的本质是什么时候是可能的。< / p >

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

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

  • 如果你先创建(或更改)业务逻辑你的应用程序,然后用测试覆盖它 (测试方法),那么它将是非常痛苦和危险的让测试知道任何关于你的软件是如何工作的,除了检查输入和输出。
  • 如果你正在练习< em > < / em >测试驱动的方法,那么你的测试就是你的软件功能的首先要编写、修改并反映用例实现依赖于测试。这有时意味着,你想要你的软件以某种特定的方式实现,例如依赖于一些其他组件的方法,甚至调用它特定的次数。这就是Mockito.verify ()派上用场的地方!

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

正如一些人所说

  1. 有时您没有可以断言的直接输出
  2. 有时,您只需要确认已测试的方法将正确的间接输出发送给协作者(您正在模拟的协作者)。
关于你在重构时担心破坏测试的问题,在使用模拟/存根/间谍时,这在一定程度上是预期的。我指的是定义,而不是一个具体的实现,如Mockito。 但是你可以这样想——如果你需要做重构,这会对你的方法的工作方式产生重大变化,用TDD方法来做是一个好主意,这意味着你可以改变你的测试第一个来定义新的行为(这会导致测试失败),而然后做这些改变并让测试再次通过

在大多数情况下,人们不喜欢使用Mockito。验证,这是因为它用于验证被测试单元正在做的所有事情,这意味着如果其中有任何变化,您将需要调整您的测试。 但是,我不认为这是个问题。如果你想改变一个方法的功能而不需要改变它的测试,这基本上意味着你想要编写测试,而不是测试你的方法所做的一切,因为你不想让它测试你的更改。

.这是一种错误的思维方式

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

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