我编写jUnit测试用例有三个目的:
- 为了确保我的代码满足所有必需的功能,在所有(或大部分)输入组合/值下
- 为了确保我可以更改实现,并依靠JUnit测试用例来告诉我,我的所有功能仍然得到满足
- 作为我的代码所处理的所有用例的文档,并作为重构的规范——如果代码需要重写的话。(重新编译代码,如果我的jUnit测试失败,你可能会错过一些用例)
我不明白为什么或何时应该使用Mockito.verify()
。当我看到verify()
被调用时,它告诉我我的jUnit正在意识到实现。(因此,即使我的功能不受影响,更改我的实现也会破坏我的jUnits)。
我正在寻找:
-
Mockito.verify()
的适当使用指南应该是什么? -
jUnits知道被测类的实现,或者与之紧密耦合,这从根本上是正确的吗?
如果类A的约定包含它调用C类型对象的方法B的事实,那么您应该通过制作C类型的mock并验证方法B是否已被调用来测试这一点。
这意味着类A的契约有足够的细节来谈论类型C(可能是接口或类)。所以,是的,我们谈论的是一个规范级别,它不仅仅是"系统需求",而且在某种程度上描述了实现。
这对于单元测试来说是正常的。当您进行单元测试时,您希望确保每个单元都在做"正确的事情",这通常包括它与其他单元的交互。这里的"单位"可能是指类,或者应用程序的较大子集。
更新:
我觉得这不仅适用于验证,也适用于存根。在某种意义上,一旦您存根了合作者类的方法,您的单元测试就变得依赖于实现。这是单元测试的本质。由于Mockito不仅是关于验证,也是关于存根,所以你使用Mockito的事实意味着你将遇到这种依赖。
根据我的经验,如果我更改类的实现,我通常必须更改其单元测试的实现才能匹配。不过,通常情况下,我不必更改类的单元测试清单;当然,除非改变的原因是我之前没有测试的条件的存在。
这就是单元测试的意义所在。一个不依赖于合作者类使用方式的测试实际上是一个子系统测试或集成测试。当然,这些也经常使用JUnit编写,并且经常涉及到嘲讽的使用。在我看来,"JUnit"是一个可怕的名字,对于一个让我们产生所有不同类型测试的产品来说。
David的答案当然是正确的,但并不能完全解释为什么要这样做。
基本上,在进行单元测试时,您是在孤立地测试一个功能单元。您可以测试输入是否产生预期的输出。有时,你也必须测试副作用。简而言之,验证可以让你做到这一点。
例如,您有一些业务逻辑,应该使用DAO来存储内容。您可以使用集成测试来实现这一点,该测试实例化DAO,将其连接到业务逻辑,然后在数据库中四处查看是否存储了预期的内容。这不再是单元测试了。
或者,您可以模拟DAO,并验证它是否以您期望的方式被调用。使用mockito,您可以验证某个东西是否被调用,它被调用的频率,甚至可以在参数上使用匹配器来确保它以特定的方式被调用。
像这样的单元测试的另一面实际上是将测试与实现联系在一起,这使得重构变得有点困难。另一方面,一个好的设计气味是它需要大量的代码来正确地执行它。如果您的测试需要很长时间,那么可能是设计出了问题。因此,需要测试的有很多副作用/复杂交互的代码可能不是一件好事。
这是个好问题!我认为它的根本原因如下,我们使用JUnit不仅仅是为了进行单元测试。所以这个问题应该分开来看:
- 我应该在集成(或任何其他高于单元测试的测试)中使用Mockito.verify()吗
- 我应该在黑盒单元测试中使用Mockito.verify()吗
- 我应该在白框单元测试中使用Mockito.verify()吗
因此,如果我们将忽略高于单元测试,这个问题可以重新表述为">使用Mockito的白盒单元测试。verify()在单元测试和我的can实现之间创建了很好的耦合,我可以做一些"灰盒">的单元测试吗?我应该对此使用什么经验法则"。
现在,让我们一步一步地完成所有这些。
*-我应该在集成(或任何其他高于单元测试的测试)中使用Mockito.verify()吗*我认为答案显然是否定的,而且你不应该对此使用嘲讽。您的测试应该尽可能接近实际应用程序。您正在测试完整的用例,而不是应用程序的孤立部分。
*黑盒与白盒如果您使用的是黑盒方法,那么您实际在做什么,您将提供(所有等价类)输入、状态,并测试您将收到预期输出。在这种方法中,通常使用mock是合理的(你只是模仿他们在做正确的事情;你不想测试他们),但调用Mockito.verify()是多余的。
如果您使用的是白盒方法,那么您实际在做什么,就是在测试您所在单位的行为。在这种方法中,调用Mockito.verify()是必不可少的,您应该验证您的单元是否按照预期运行
灰盒测试的拇指规则白盒测试的问题在于它会产生高耦合。一种可能的解决方案是进行灰盒测试,而不是白盒测试。这是黑色和黑色的组合;白盒测试。您确实在测试单元的行为,就像白盒测试一样,但通常情况下,您会在可能的情况下使其与实现无关。当可能的时候,你只需要像黑匣子一样做一个检查,就可以断言输出是你期望的。所以,你的问题的本质是什么时候可能。
这真的很难。我没有一个好的例子,但我可以给你举一些例子。在上面提到的equals()与equalsIgnoreCase()的情况下,您不应该调用Mockito.verify(),只需断言输出即可。如果你做不到,就把你的代码分解成更小的单元,直到你能做到为止。另一方面,假设你有一些@Service,并且你正在编写@Web Service,它本质上是对你的@Service的包装-它将所有调用委托给@Service(并进行一些额外的错误处理)。在这种情况下,调用Mockito.verify()是必不可少的,您不应该重复您为@Serive所做的所有检查,验证您调用的@Service具有正确的参数列表就足够了。
我必须说,从经典方法的角度来看,你是绝对正确的:
- 如果您首先创建(或更改)应用程序的业务逻辑,然后使用(采用)测试(测试最后一种方法)来覆盖它,那么让测试知道您的软件如何工作将是非常痛苦和危险的,而不是检查输入和输出
- 如果您正在实践测试驱动方法,那么您的测试是第一个被编写、被更改并反映软件功能用例的测试实现取决于测试这有时意味着,你希望你的软件以某种特定的方式实现,例如,依赖于其他组件的方法,甚至调用它特定的次数。这就是Mockito.verify()派上用场的地方
重要的是要记住,没有通用工具。软件的类型、规模、公司目标和市场状况、团队技能和许多其他因素都会影响在特定情况下使用哪种方法的决定。
在大多数情况下,当人们不喜欢使用Mockito.verify时,这是因为它用于验证被测试单元正在做的一切,这意味着如果测试发生任何变化,您需要调整测试。但是,我不认为这是个问题。如果你想在不需要改变测试的情况下改变一个方法的功能,这基本上意味着你想编写测试,而不是测试你的方法正在做的一切,因为你不想让它测试你的更改。这是错误的思维方式。
真正的问题是,如果你能修改你的方法,并且一个本应完全覆盖功能的单元测试不会失败。这意味着,无论你的改变是什么意图,你的改变的结果都不在测试范围内。
正因为如此,我更喜欢尽可能多地模拟:也模拟您的数据对象。在执行此操作时,您不仅可以使用verify来检查是否调用了其他类的正确方法,还可以检查是否通过这些数据对象的正确方法收集了正在传递的数据。为了使其完整,您应该测试调用发生的顺序。示例:如果修改一个数据库实体对象,然后使用存储库进行保存,则仅验证是否使用正确的数据调用了该对象的setter以及是否调用了存储库的保存方法是不够的。如果它们以错误的顺序调用,那么您的方法仍然不能完成它应该执行的操作。因此,我不使用Mockito.verify,而是创建了一个包含所有mock的inOrder对象,并使用inOrder.verify。如果你想让它完整,你也应该在最后调用Mockito.verifyNoMoreInteractions,并将所有的mock都传递给它。否则,有人可以在不测试的情况下添加新的功能/行为,这意味着在你的覆盖率统计数据为100%之后,你仍然在堆积未断言或验证的代码。
正如一些人所说的
- 有时您没有可以断言的直接输出
- 有时,您只需要确认您的测试方法正在向其合作者发送正确的间接输出(您正在嘲笑这一点)
关于重构时破坏测试的问题,在使用mock/stubs/spees时,这是意料之中的事。我的意思是,这是定义,而不是关于像Mockito这样的特定实现。但你可以这样想——如果你需要进行重构,对你的方法的工作方式进行重大更改,那么用TDD方法进行重构是个好主意,这意味着你可以先更改测试,定义新的行为(这将使测试失败),然后进行更改,再次通过测试。