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