在Mockito中,验证与被模拟方法相同的方法是否有必要?

12

我经常看到在Mockito中模拟的方法被验证时也会被验证(如下面的例子)。 在这些情况下调用Mockito.verify()有什么额外的好处吗?

//mock method
FooService fs = mock(FooService.class);
when(fs.getFoo()).thenReturn("foo");

//method under test
fs.doSomething();

//verify method
verify(fs).getFoo();

如果没有调用fs.getFoo(),该方法应该会失败。那么为什么要调用verify?除了ArgumentCaptor情况下需要在verify中断言参数之外,还有必要吗?


我认为这是必要的,因为如果您突然更改了doSomething()不再调用getFoo()或者只是将其注释掉,那么怎么知道呢?如果您没有验证方法调用,那么您将无从得知。 - Taj Ahmed
1
@TajAhmed 如果不使用 verify(),有没有办法使测试失败,如果模拟的方法未被调用? - Glide
1
当您在测试类方法中使用模拟方法的输出时,这种情况有点可能发生。然后,您只需通过断言返回值来验证准备好了模拟数据。这样做可能取决于您的测试方法是否返回任何值。因此,我建议使用verify来确保方法被调用。 - Taj Ahmed
我认为你应该使用术语“存根化(stubbed)”而不是“模拟(mocked)”来描述Mockito.when(参见http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html#2)。 - aro_tech
2个回答

13
The Mockito文档反复强调它经常是多余的。这在verify(T)的Javadoc中以逐字逐句的形式出现,也在Mockito的主类Javadoc第二部分的代码块中作为多个单行注释出现。

虽然可以验证存根调用,但通常只是多余的。 如果您的代码关心get(0)返回的内容,那么其他问题会出现(通常甚至在执行verify()之前就出现)。 如果您的代码不关心get(0)返回的内容,那么它就不应该被存根。还不信?请看这里

请注意,最初链接的文章 "Asking and Telling" 是由Mockito的创始人Szczepan Faber撰写的,可以被视为Mockito设计方面的权威文件。以下是该文章的摘录:
我真的需要重复相同的表达吗?毕竟,存根交互是隐式验证的。我的代码执行流完全免费完成了这项工作。Aaron Jensen也注意到:
如果您正在验证,则不需要存根,除非该方法返回对测试(或代码)的流程至关重要的内容,在这种情况下,您确实不需要验证,因为流程已经得到验证。
简而言之:没有重复的代码。
但是,如果一个有趣的交互共享了询问和告知的特征呢?我需要在stub()和verify()中重复交互吗?我会得到重复的代码吗?并不是真的。实际上:如果我存根,那么它就是免费验证的,所以我不验证。如果我验证,那么我不关心返回值,所以我不存根。无论哪种方式,我不会重复自己。然而,在理论上,我可以想象一种罕见的情况,即验证已存根的交互,例如确保存根的交互发生了n次。但这是行为的另一个方面,显然是一个有趣的方面。因此,我想明确表示,并且我非常乐意牺牲一行额外的代码...

自这个问题发布以来,Mockito的最新版本(发布后)已经添加了一些额外的功能,允许默认为更严格的模拟风格。尽管如此,普遍的期望是通过仅验证无法通过断言或成功测试完成确认的内容来避免脆弱性。

总体而言,Mockito的设计是尽可能地使测试具有灵活性,不是编码实现,而是针对正在测试的方法的规范。虽然您偶尔会看到一个方法调用作为函数规范的一部分(“向服务器提交RPC”或“立即调用传递的LoginCallback”),但更有可能的是您希望验证可以从存根中推断出的后置条件:检查是否调用了getFoo并不是规范的一部分,只要您将getFoo存根为返回“foo”,并且数据存储器包含一个其对应属性设置为“foo”的单个对象。


简而言之,显式验证只能从精心制作的存根和后置条件断言中无法推导出的交互是良好的Mockito风格。它们可能是用于无法测量的副作用(如日志记录代码、线程执行器、ArgumentCaptors、多个方法调用、回调函数)的良好调用,但通常不应用于存根化的交互。

“询问和告知”链接已失效。没有这个链接,我感到困惑,因为测试的目的是测试代码中的意外更改是否会导致测试失败。而这个概念似乎为了减少口头测试而牺牲了它?许多其他模拟框架可以用一行代码完成这两个任务,无需重复。 - Eaton Emmerich
1
@EatonEmmerich 我已经更新了链接。Mockito框架最初的观点是,意外的代码更改不应该自动使测试失败,因为一个好的测试不应该是一个变化检测器,它只应该将实现保持在其自身规范下,即使实现发生变化。像Mockito的灵感/祖先EasyMock一样,可以默认情况下拥有严格的模拟,但Mockito故意分歧了这个原因。自我回答后,Mockito的新概念“宽容”确实修改了原始愿景,但这超出了我今晚的编辑范围。 - Jeff Bowman
我找到了“询问和告知”文章的链接。看起来Faber旧博客的域名已经更改,而他的新博客并不包括这些旧帖子,这一事实使得情况变得有些复杂。链接为https://szczepiq.wordpress.com/2008/04/26/asking-and-telling/。 - Planky
@Planky 谢谢,是的。URL架构有点变化,但帖子仍然存在。我已经重新编辑了最新的帖子。 - Jeff Bowman

0
在你给出的具体示例中(虽然它并不是一个完整的示例,但足以使你的问题明确),我看不出需要验证存根调用的任何理由。
我认为良好的设计是验证旨在产生副作用的调用,并使用when存根旨在提供数据的调用。在大多数情况下,同时执行两者会使设计变得不太清晰,测试稍微不易读取。
如果接收到的参数值是由待测方法生成或修改的(例如,合成的键来检索值),则即使参数足够简单,可以不需要ArgumentCaptor,也可能有用。您可以使用特定参数或自定义匹配器进行存根,这可能足以测试这种情况(例如when(fs.getFoo(“generatedKey1234”))。thenReturn(“foo1234”)),但在verify中检查参数可能会增强测试的可读性-因此这是一个判断性的问题。

你能详细解释一下这句话吗:“如果一个提供数据且没有副作用的调用接收到参数值…”?我不太理解这部分。 - Glide
如果你的测试方法(我们称之为MUT)调用了合作者的getter方法(例如你的示例中的getFoo()),你只需要stub getter来验证MUT的行为是否符合预期。如果MUT调用协作者根据传递给MUT的参数检索值(例如根据用户名从存储中检索用户),那么这也是一个明确的stubbing案例,因为MUT只是获取并使用给定的数据从协作者中获取数据。 - aro_tech
1
继续我之前的评论,你不清楚的情况是:MUT并不只是将接收到的参数传递给协作者,而是实际上修改它们或生成它们或从协作者获取它们。 MUT创建或检索了至少一个在调用之前它没有的值,并使用该值从协作者中检索数据(对于测试进行了模拟)。在这种情况下,您需要验证新创建/检索的值(它本身不是由MUT返回)是否正确创建或检索。 - aro_tech

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接