Mockito Verify方法如何工作?

时间:2019-07-20 12:53:37

标签: java unit-testing testing mocking mockito

我正在搜索如何测试是否已正确地从数据库中删除某些内容,但我找到了以下答案:https://stackoverflow.com/a/38082803/9115438,但让我开始思考,如果delete方法失败并且实际上并未删除该对象,然后怎样呢? verify方法仅检查delete方法是否被调用一次或被调用一次且是否成功? 因为如果不检查删除是否成功,则该测试根本没有用。

2 个答案:

答案 0 :(得分:2)

您的观点很重要。
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格式描述/复制您方法的实现。
它没有根据返回结果声明任何内容。以错误的方式更改实现(添加错误的调用或删除所需的调用)很难通过测试失败来检测,因为测试只是反映了所调用的语句。

通常要谨慎使用模拟验证。
在某些特定情况下,这可能会有所帮助,但在许多情况下,您只需要维护这些无助的代码,并且由于种种原因使构建速度变慢。
实际上,对于实质上依赖于具有副作用的功能的测试方法,应该支持集成测试(甚至是切片/部分测试)。

Mocks Aren't Stubs of Martin Fowler是您应该感兴趣的优秀文章。

  

当我观察到一个嘲笑者时,这尤其让我震惊   程序员。我真的很喜欢在编写测试时   关注行为的结果,而不是行为的方式。一个嘲笑者是   不断思考如何在以下环境中实现SUT   为了写出期望。这对我来说真的很不自然。

答案 1 :(得分:2)

您指的是Junit: writing a test for a method that deletes an entity?中的示例:

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可能会失败,那么,我们首先会有一个集成问题:开发人员的单元测试将在{{1} }永不失败。而且,在使用(不完全)模拟的delete进行单元测试期间,不会发现这种误解。同样,在集成测试期间必须遇到delete在运行时可能失败的情况。然后,再次必须扩展示例代码,将扩展单元测试,等等。


编辑:以上内容旨在说明单元测试和集成测试之间的关系以及模拟的作用。 davidxxx正确指出的是,对于这段示例代码,单元测试将没有太大的价值:该代码本质上是由交互组成的-单元测试无法捕获错误。因此,对于此示例代码,测试应立即从集成测试开始。