Mockito的Verify方法如何工作?

5
我在搜索如何测试数据库中的内容是否被正确删除时,发现了这个答案:https://dev59.com/e5jga4cB1Zd3GeqPKXJJ#38082803。但是这让我想到,如果删除方法失败并且它实际上没有删除对象,那该怎么办呢?验证方法只检查删除方法是否被调用一次,还是检查它被调用一次且成功了?因为如果它不检查删除是否成功,那么这个测试就毫无意义了。

3
通常在使用Mockito时,您希望执行您实现的逻辑的单元测试。因此,您只想检查您的代码是否在某些情况下调用了DB组件,并且您不关心保存是否正确 - 这将是一个集成测试。所以是的,verify仅检查您的代码与创建的模拟对象的交互。 - Michał Krzywański
2
非常好的问题。 - davidxxx
2个回答

4
您所提到的观点很相关。
Mockito.verify()仅会验证在方法执行期间是否对模拟对象进行了调用。
这意味着,如果模拟类的真实实现无法按照预期工作,则测试是无济于事的。
因此,在测试中模拟的类/方法也必须进行单元测试。
但这是否意味着verify()总是可行?实际上并非如此。

假设要测试删除某些内容的方法:

public class Foo{
    MyDao myDao;

    public void delete(int id){
       myDao.delete(id);
    }
}

测试通过:
@Mock
MyDao myDaoMock;
Foo foo;

@Test
public void delete(int id){
   foo.delete(id);
   Mockito.verify(myDaoMock).delete(id);
}

假设现在我将实现方式更改为:
public void delete(int id){
   myDao.delete(id);
   myDao.create(id);
}

测试仍然是绿色的...哎呀。

另一种情况,假设一个主要调用依赖方法的方法:

public void doThat(){
   Foo foo = fooDep.doThat(...);
   Bar bar = barDep.doThat(foo);
   FooBar fooBar = fooBarDep.doThat(foo, bar);
   fooBis.doOtherThing(...);
   // and so for
}

使用验证方法,单元测试将只是以Mockito格式描述/复制实现您的方法。
它不对返回结果进行任何断言。以不良方式更改实现(添加不正确的调用或删除所需的调用)很难通过测试失败检测到,因为测试成为了被调用语句的反映。

Mock验证通常需要谨慎使用。
在某些特定情况下,它可能会有所帮助,但在许多情况下,我看到开发人员滥用它(75%或更多的单元测试类是mock设置/记录/验证),结果产生了少量价值、很难维护且没有公平原因使构建变慢的膨胀单元测试。
事实上,对于主要依赖于具有副作用函数的方法,应优先使用集成测试(即使是部分切片的集成测试)。


Mocks Aren't Stubs of Martin Fowler 是一篇非常好的文章,值得你关注。

这尤其让我印象深刻,因为我观察到了一个模拟者程序员。我真的很喜欢编写测试时,你关注的是行为的结果,而不是它的实现方式。模拟者一直在考虑SUT将如何被实现,以便编写预期结果。这对我来说感觉非常不自然。

尽管这篇Martin Fowler文章很有趣,但我也不完全同意。
我同意Mockist方法,其中开发人员不mock依赖项,因为依赖项不方便,但系统地这样做通常是一个不好的主意。
我们应该始终有一个好的理由引入一个mock,例如:

  • 外部系统依赖项
  • 调用缓慢
  • 可重复性问题
  • 与依赖项交互的复杂设置(输入和/或输出)

但我不同意显式地创建存根通常是一个好主意,因为存根代码需要花费时间编写,可能会有错误,我们将不得不维护它。最后,为了使事情变得清晰稳健,存根类也应该进行单元测试。所有这些都有成本。
另一方面,由模拟库生成的Mocks没有所有这些缺陷。
并且没有什么阻止我们使用这些Mocks并具有Stub的思维方式:使Mocks协作为存根,即:

  • 是的,用于输入/输出记录
  • 是的,用于少量且相关的 `verify()`
  • 但不要滥用 `verify()`
  • 以及不要使用复杂的模拟行为记录,这将把测试与实现耦合在一起

3
您正在参考Junit:编写删除实体方法的测试用例的示例吗?
public void deleteFromPerson(person person) {
    person = personRepository.returnPerson(person.getId());
    personRepository.delete(person);
}

而且,你是对的,在单元测试中模拟delete方法时,你只能确定是否调用了delete,而不能确定删除是否成功。那么,delete失败的两种可能性是什么?
a)delete没有正确地调用,可能缺少某些参数或需要做一些准备工作才能进行删除。这种问题无法通过单元测试找到:如果你对如何调用另一个组件的假设是错误的,并且你模拟了另一个组件,那么你实现模拟的方式将仅反映你自己的误解,并且单元测试将成功。然而,这不是单元测试的缺陷:这就是为什么除了单元测试外还存在其他级别(如集成测试)的原因,它们旨在找到单元测试无法找到的那些缺陷。实际上,集成测试旨在找到组件之间交互中的缺陷。
b)delete可能在运行时失败,无论你是否正确调用了delete方法。例如,你的代码可能没有写入personRepository的权限,或者某个并行线程在此期间删除了该人员,等等。然而,示例代码中没有采取任何措施处理这样的运行时情况(好吧,它只是一个示例代码片段,但也可能是有意为之,见davidxxx的评论)。但是,假设应该有一些代码来处理未成功的delete
当正确进行模拟时(即查看delete的规范),则已经在单元测试期间可以明显地发现delete可能会失败。在这种情况下,也许它会返回错误代码或抛出异常。当开发人员意识到这一点时,就可以决定通过相应的错误处理代码扩展上述示例代码。而且,错误处理代码可以通过模拟delete来进行单元测试,以便还可以练习这些错误情况。
相反,如果开发人员没有意识到delete可能会失败,那么我们首先会遇到集成问题:开发人员的单元测试将根据delete永远不会失败的假设进行实现。并且这种误解不会在(不完全)模拟的delete的单元测试中找到。同样,它将不得不在集成测试期间遇到delete可能在运行时失败的情况。然后,再次需要扩展示例代码,并扩展单元测试等 。

虽然我们同意单元测试只能保证集成测试的一致性,但并不认为在集成测试中检测到PersonRepository.delete()问题后单元测试一定会改变。这种情况可能发生,但相对较少见。对于任何存储库调用都进行try/catch将使客户端代码混乱,并且可能会做同样的事情。在大多数情况下,我们没有针对编码或网络问题的特定异常处理。因此,我会让异常传播到上层,并为其提供通用处理。当然,这是个例外情况。 - davidxxx

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