【问题标题】:How does the Mockito Verify method works?Mockito 验证方法如何工作?
【发布时间】:2019-07-20 12:53:37
【问题描述】:

我正在搜索如何测试是否从数据库中正确删除了某些内容,我找到了这个答案:https://stackoverflow.com/a/38082803/9115438 但它让我思考,如果删除方法失败并且它实际上并没有删除对象,那该怎么办? verify 方法是否只检查 delete 方法是否被调用一次,或者它是否被调用一次并且成功? 因为如果它不检查删除是否成功,那么该测试根本就没有用。

【问题讨论】:

  • 通常,当您使用 Mockito 时,您希望对您实现的逻辑执行单元测试。因此,您只想检查您的代码是否在某些情况下调用 DB 组件,而您并不关心保存是否正确 - 这将是一个集成测试。所以是的 - 验证只检查您的代码与您创建的模拟的交互。
  • 非常非常好的问题。

标签: java unit-testing testing mocking mockito


【解决方案1】:

您的观点是相关的。
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 格式描述/复制您方法的实现。
它对返回的结果没有任何断言。以不好的方式更改实现(添加不正确的调用或删除所需的调用)很难检测到测试失败,就像测试只是被调用语句的反映。

模拟验证通常需要谨慎使用。
在某些特定情况下,它可能会有所帮助,但在许多情况下,我看到开发人员滥用它(75% 或更多的单元测试类是模拟设置/记录/验证),因此它产生了一个膨胀的单元测试价值很少,很难维护,而且由于不公平的原因也会减慢您的构建速度。
事实上,对于本质上依赖于具有副作用的函数的测试方法,应该优先考虑集成测试(甚至切片/部分)。


Mocks Aren't Stubs of Martin Fowler 是一篇很棒的帖子,你应该会感兴趣。

当我观察到一个模仿者时,这让我特别震惊 程序员。我真的很喜欢这样一个事实,即在编写测试时你 关注行为的结果,而不是如何完成。一个嘲笑者是 不断思考 SUT 将如何在 为了写期望。这对我来说真的很不自然。

虽然 Martin Fowler 的这篇文章很有趣,但我也不完全同意。
我同意开发人员不模拟依赖项的模拟方法,因为依赖项很烦人,但系统地这样做是通常是个坏主意
我们应该总是有充分的理由引入一个模拟,例如:

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

但我不同意创建显式存根通常是一个好主意,因为存根代码需要时间来编写,可能有错误,我们必须维护它。最后,为了使事情变得干净健壮,存根类也应该进行单一测试。所有这些都是有代价的。
另一方面,由 mocking 库生成的 mock 没有所有这些缺陷。
没有什么能阻止我们带着存根使用这些模拟:让模拟作为存根进行协作,即:

  • 是的,用于输入/输出记录
  • 是的,少数和相关verify()
  • verify() 滥用则不行
  • 不用于将测试与实施相结合的细粒度模拟行为记录的乘法

【讨论】:

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


    编辑:以上是为了解释单元测试和集成测试之间的关系以及模拟的作用。 davidxxx 正确指出的是,对于这段示例代码,单元测试没有太大价值:代码本质上是由交互组成的——没有单元测试可以捕获错误的计算。因此,对于本示例代码,测试应立即从集成测试开始。

    【讨论】:

    • 同意这里的唯一保证是集成测试,但不同意在集成测试中检测到PersonRepository.delete() 上的问题后该单元测试必然会改变。这是可能的,但这种情况相当罕见。为任何存储库调用执行try/catch 将使客户端代码混乱,异常处理可能会做同样的事情。在大多数情况下,我们没有针对编码或网络问题的特定异常处理。所以我会让异常在上面传播,并为此进行通用处理。但当然是例外情况。
    猜你喜欢
    • 1970-01-01
    • 2015-02-11
    • 1970-01-01
    • 1970-01-01
    • 2022-07-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多