何时使用 Mockito.verify()?
Posted
技术标签:
【中文标题】何时使用 Mockito.verify()?【英文标题】:When to use Mockito.verify()? 【发布时间】:2012-09-14 09:32:49 【问题描述】:我为 3 个目的编写 jUnit 测试用例:
-
为了确保我的代码在所有(或大部分)输入组合/值下满足所有必需的功能。
为了确保我可以更改实现,并依靠 JUnit 测试用例告诉我我的所有功能仍然得到满足。
作为我的代码处理的所有用例的文档,并充当重构规范 - 如果代码需要重写。 (重构代码,如果我的 jUnit 测试失败 - 你可能错过了一些用例)。
我不明白为什么或何时应该使用Mockito.verify()
。当我看到 verify()
被调用时,它告诉我我的 jUnit 正在意识到该实现。 (因此更改我的实现会破坏我的 jUnit,即使我的功能不受影响)。
我正在寻找:
正确使用Mockito.verify()
的准则应该是什么?
jUnit 了解或紧密耦合到被测类的实现是否从根本上正确?
【问题讨论】:
我尽量避免使用 verify() ,原因与您公开的原因相同(我不希望我的单元测试知道实现),但是a 是我别无选择的情况 - 存根无效方法。一般来说,由于它们不返回任何内容,因此它们不会对您的“实际”输出做出贡献;但是,您仍然需要知道它被调用了。但我同意你的观点,使用 verify 来验证执行流程是没有意义的。 【参考方案1】:如果类 A 的契约包括它调用 C 类型对象的方法 B 的事实,那么您应该通过制作 C 类型的模拟来测试这一点,并验证方法 B 是否已被调用。
这意味着类 A 的契约有足够的细节来讨论类型 C(可能是接口或类)。所以是的,我们谈论的规范级别超出了“系统要求”,并且在某种程度上描述了实现。
这对于单元测试是正常的。当您进行单元测试时,您希望确保每个单元都在做“正确的事情”,这通常包括它与其他单元的交互。这里的“单元”可能是指类或应用程序的更大子集。
更新:
我觉得这不仅适用于验证,也适用于存根。一旦您存根协作者类的方法,您的单元测试就在某种意义上变得依赖于实现。这有点像单元测试的性质。由于 Mockito 与验证一样重要,因此您使用 Mockito 的事实意味着您将遇到这种依赖关系。
根据我的经验,如果我改变一个类的实现,我经常不得不改变它的单元测试的实现来匹配。不过,通常情况下,我不必更改类是的单元测试清单;当然,除非更改的原因是存在我之前未能测试的条件。
这就是单元测试的意义所在。不受这种对协作者类使用方式的依赖影响的测试实际上是子系统测试或集成测试。当然,这些也经常用 JUnit 编写,并且经常涉及到 mocking 的使用。在我看来,“JUnit”是一个糟糕的名字,因为它可以让我们进行所有不同类型的测试。
【讨论】:
谢谢,大卫。在扫描了一些代码集之后,这似乎是一种常见的做法——但对我来说,这违背了创建单元测试的目的,并且只是增加了维护它们的开销,而且价值很小。我确实理解为什么需要模拟,以及为什么需要设置执行测试的依赖项。但是在我看来,验证方法 dependencyA.XYZ() 是否被执行会使测试变得非常脆弱。 @Russell 即使“C 型”是围绕库或应用程序的某些不同子系统的包装器的接口? 我不会说确保调用某些子系统或服务是完全没有用的——只是应该有一些指导方针(制定它们是我想做的)。例如:(我可能过于简单)说,我在我的代码中使用 StrUtil.equals(),并决定在实现中切换到 StrUtil.equalsIgnoreCase()。如果 jUnit 有验证(StrUtil.equals ),尽管实现是准确的,但我的测试可能会失败。这个验证调用,IMO,是不好的做法,虽然它是针对库/子系统的。另一方面,使用 verify 来确保调用 closeDbConn 可能是一个有效的用例。 我理解你并完全同意你的看法。但我也觉得编写您描述的指南可以扩展到编写完整的 TDD 或 BDD 教科书。举个例子,调用equals()
或equalsIgnoreCase()
永远不会是在类的要求中指定的东西,因此本身永远不会有单元测试。但是,“完成后关闭数据库连接”(无论这在实现方面意味着什么)很可能是一个类的要求,即使它不是“业务要求”。对我来说,这归结为合同之间的关系......
... 在业务需求中表达的类,以及对该类进行单元测试的测试方法集。在任何有关 TDD 或 BDD 的书籍中,定义这种关系都是一个重要主题。虽然 Mockito 团队中的某个人可以为他们的 wiki 写一篇关于这个主题的帖子,但我看不出它与许多其他可用的文献有何不同。如果您发现它可能有什么不同,请告诉我,也许我们可以一起努力。【参考方案2】:
大卫的回答当然是正确的,但并不能完全解释你为什么想要这个。
基本上,在进行单元测试时,您是在单独测试一个功能单元。您测试输入是否产生预期的输出。有时,您还必须测试副作用。简而言之,验证允许您这样做。
例如,您有一些应该使用 DAO 存储内容的业务逻辑。您可以使用一个集成测试来实例化 DAO,将其连接到业务逻辑,然后在数据库中查看是否存储了预期的内容。这不再是单元测试。
或者,您可以模拟 DAO 并验证它是否以您期望的方式被调用。使用 mockito,您可以验证某个东西是否被调用、调用频率,甚至可以在参数上使用匹配器以确保它以特定方式被调用。
像这样的单元测试的另一面确实是您将测试与实现联系起来,这使得重构更加困难。另一方面,良好的设计气味是正确执行它所需的代码量。如果您的测试需要很长,则可能是设计有问题。所以有很多副作用/需要测试的复杂交互的代码可能不是一件好事。
【讨论】:
【参考方案3】:这是个好问题! 我认为它的根本原因如下,我们不仅使用 JUnit 进行单元测试。所以这个问题应该分开:
我应该在我的集成(或任何其他高于单元测试)测试中使用 Mockito.verify() 吗? 我应该在我的 black-box 单元测试中使用 Mockito.verify() 吗? 我应该在我的 white-box 单元测试中使用 Mockito.verify() 吗?因此,如果我们忽略高于单元的测试,则可以将问题改写为“使用带有 Mockito.verify() 的 white-box 单元测试在单元测试之间创建了很好的组合以及我的可能实现,我可以进行一些 “灰盒” 单元测试以及我应该为此使用哪些经验法则”。
现在,让我们一步一步地完成所有这些。
*- 我应该在我的集成(或任何其他高于单元测试)测试中使用 Mockito.verify() 吗?* 我认为答案显然是否定的,而且你不应该为此使用模拟。您的测试应尽可能接近实际应用。您正在测试完整的用例,而不是应用程序的孤立部分。
*黑盒 vs 白盒单元测试* 如果您使用 black-box 方法,您实际上在做什么,您提供(所有等价类)输入、state,并测试您将收到预期的输出。在这种方法中,使用模拟通常是合理的(你只是模仿他们正在做正确的事情;你不想测试它们),但调用 Mockito.verify() 是多余的。
如果您正在使用 white-box 方法,那么您正在测试您的单元的行为。在这种方法中调用 Mockito.verify() 是必不可少的,您应该验证您的单元的行为是否符合您的预期。
灰盒测试的经验法则 白盒测试的问题在于它会产生高耦合。一种可能的解决方案是进行灰盒测试,而不是白盒测试。这是一种黑白盒测试的组合。您实际上是在测试您的单元的行为,就像在白盒测试中一样,但一般来说,尽可能使其与实现无关。如果可能,您只需像在黑盒情况下一样进行检查,只需断言输出是您所期望的。所以,你的问题的本质是什么时候可能。
这真的很难。我没有很好的例子,但我可以给你举个例子。在上面提到的 equals() 与 equalsIgnoreCase() 的情况下,您不应该调用 Mockito.verify(),只需断言输出即可。如果你做不到,把你的代码分解成更小的单元,直到你能做到为止。另一方面,假设您有一些@Service,并且您正在编写@Web-Service,它本质上是您的@Service 的包装器——它将所有调用委托给@Service(并进行一些额外的错误处理)。在这种情况下,调用 Mockito.verify() 是必不可少的,你不应该重复你对 @Serive 所做的所有检查,验证你使用正确的参数列表调用 @Service 就足够了。
【讨论】:
灰盒测试有点坑。我倾向于将其限制在 DAO 之类的东西上。由于大量的灰盒测试,几乎完全缺乏单元测试以及太多的黑盒测试来弥补对灰盒测试所谓的测试内容缺乏信任,我参与了一些构建速度极慢的项目。 对我来说这是最好的答案,因为它回答了在各种情况下何时使用 Mockito.when() 。干得好。【参考方案4】:我必须说,从经典方法的角度来看,您是绝对正确的:
如果您首先创建(或更改)应用程序的业务逻辑,然后用(采用)测试覆盖它(Test-Last 方法),那么除了检查输入和输出之外,让测试知道你的软件是如何工作的任何事情都是非常痛苦和危险的。 如果您正在练习Test-Driven approach,那么您的测试就是您的软件功能的first to be written, to be changed and to reflect the use cases。 实施取决于测试。 这有时意味着您希望以某种特定方式实施您的软件,例如依赖于其他组件的方法,甚至调用它特定的次数。这就是Mockito.verify() 派上用场的地方!重要的是要记住,没有通用工具。软件的类型、它的规模、公司目标和市场情况、团队技能和许多其他因素都会影响在您的特定情况下使用哪种方法的决定。
【讨论】:
【参考方案5】:有人说
-
有时您没有可以断言的直接输出
有时您只需要确认您的测试方法将正确的间接输出发送给其协作者(您正在模拟)。
关于在重构时破坏测试的担忧,在使用 mocks/stubs/spies 时这是意料之中的。我的意思是根据定义,而不是针对特定的实现,例如 Mockito。 但是您可以这样想——如果您需要进行重构以对您的方法的工作方式产生重大改变,那么最好在 TDD 方法上进行,这意味着您可以先更改测试 来定义新的行为(这将导致测试失败),然后然后进行更改并再次通过测试。
【讨论】:
【参考方案6】:在大多数情况下,当人们不喜欢使用 Mockito.verify 时,这是因为它用于验证被测试单元正在执行的所有操作,这意味着如果其中有任何更改,您将需要调整您的测试。 但是,我认为这不是问题。如果您希望能够更改方法的功能而无需更改其测试,这基本上意味着您要编写不测试您的方法所做的所有事情的测试,因为您不希望它测试您的更改.这是错误的思维方式。
真正的问题是,如果您可以修改您的方法的功能,并且应该完全涵盖该功能的单元测试不会失败。这意味着无论您的更改意图是什么,您的更改结果都不会被测试覆盖。
因此,我更喜欢尽可能多地模拟:同时模拟您的数据对象。这样做时,您不仅可以使用 verify 来检查是否调用了其他类的正确方法,而且还可以使用这些数据对象的正确方法收集传递的数据。为了使其完整,您应该测试调用发生的顺序。 示例:如果您修改 db 实体对象,然后使用存储库将其保存,则仅验证对象的 setter 是否使用正确的数据调用以及是否调用了存储库的 save 方法是不够的。如果它们以错误的顺序被调用,你的方法仍然没有做它应该做的事情。 所以,我不使用 Mockito.verify 但我创建了一个包含所有模拟的 inOrder 对象并使用 inOrder.verify 代替。如果你想让它完整,你还应该在最后调用 Mockito.verifyNoMoreInteractions 并将所有的模拟传递给它。否则,有人可以在不测试的情况下添加新功能/行为,这意味着在您的覆盖率统计可以达到 100% 之后,您仍然在堆积未断言或验证的代码。
【讨论】:
以上是关于何时使用 Mockito.verify()?的主要内容,如果未能解决你的问题,请参考以下文章