这是Mockito的reset方法的适当用法吗?


68

我的测试类中有一个私有方法,该方法构造一个常用Bar对象。Bar构造函数someMethod()在我的模拟对象中调用方法:

private @Mock Foo mockedObject; // My mocked object
...

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
}

在我的某些测试方法中someMethod,该特定测试也调用了我要检查的方法。类似于以下内容:

@Test
public void someTest() {
  Bar bar = getBar();

  // do some things

  verify(mockedObject).someMethod(); // <--- will fail
}

这将失败,因为模拟对象已someMethod被调用两次。我不想让我的测试方法关心方法的副作用getBar(),所以在末尾重置模拟对象是否合理getBar()

private Bar getBar() {
  Bar result = new Bar(mockedObject); // this calls mockedObject.someMethod()
  reset(mockedObject); // <-- is this OK?
}

我问,因为文档建议重置模拟对象通常表示不良测试。但是,这对我来说感觉还可以。

另类

替代选择似乎是在调用:

verify(mockedObject, times(2)).someMethod();

在我看来,这迫使每个测试都知道对的期望getBar(),但没有收获。

Answers:


60

我相信这是可以使用的情况之一reset()。您正在编写的测试正在测试“某些事物”触发对的单个调用someMethod()verify()用不同数量的调用编写语句可能导致混乱。

  • atLeastOnce() 允许误报,这是一件坏事,因为您希望测试始终正确。
  • times(2)可以防止误报,但可以使您看起来好像期望两个调用,而不是说“我知道构造函数添加了一个”。此外,如果构造函数发生了变化以添加额外的调用,则测试现在有机会出现误报。并且删除呼叫将导致测试失败,因为现在测试是错误的,而不是所测试的是错误的。

通过使用reset()辅助方法,可以避免这两个问题。但是,您需要注意,它也会重设您所做的任何存根,因此请注意。reset()不鼓励的主要原因是为了防止

bar = mock(Bar.class);
//do stuff
verify(bar).someMethod();
reset(bar);
//do other stuff
verify(bar).someMethod2();

这不是OP想要做的。我假设OP有一个测试可以验证构造函数中的调用。对于此测试,重置允许隔离此单个动作及其效果。这与少数情况之一reset()可能会有所帮助。不使用它的其他选项都有缺点。OP担任此职位的事实表明,他正在考虑这种情况,而不仅仅是盲目地使用重置方法。


17
我希望Mockito提供了resetInteractions()调用,只是为了验证(...,times(...))而忘记了过去的交互并保持存根。这将使{setup; 法案; 验证;}这更容易处理。这将是{setup; resetInteractions; 法案; 验证}
Arkadiy 2014年

2
实际上,从Mockito 2.1开始,它确实提供了一种无需重新设置存根即可清除调用的方法:Mockito.clearInvocations(T... mocks)
Colin D Bennett

6

Smart Mockito用户几乎不使用重置功能,因为他们知道重置功能可能表明测试不佳。通常,您无需重置模拟,只需为每个测试方法创建新的模拟即可。

而不是reset()考虑在冗长的,过度指定的测试中编写简单,小型且集中的测试方法。第一种潜在的代码气味reset()在测试方法的中间。

模仿文档中提取。

我的建议是,您应避免使用reset()。我认为,如果您两次调用someMethod,则应该对其进行测试(也许是数据库访问或您要注意的其他长过程)。

如果您真的不在乎,可以使用:

verify(mockedObject, atLeastOnce()).someMethod();

请注意,如果您从getBar调用someMethod而不是在after之后调用,这可能会导致错误的结果(这是错误的行为,但测试不会失败)。


2
是的,我已经看到了确切的报价(我从问题中链接了该报价)。目前,关于我上面的示例为什么是“不好的”,我还没有看到像样的争论。你能提供一个吗?
Duncan Jones

如果您需要重置模拟对象,似乎您正在尝试测试太多内容。您可以将其分为两个测试,以测试较小的内容。无论如何,我不知道为什么要在getBar方法中进行验证,很难跟踪要测试的内容。我建议您设计测试思路,考虑您的班级应该做什么(如果您必须精确地两次调用someMethod,至少一次,一次,从不等),并在同一位置进行每个验证。
greuze

我已经对问题进行了编辑,以突出表明即使我未调用verify私有方法(我同意,可能不属于该方法),问题仍然存在。我欢迎您对您的答案是否会有所改变发表评论。
Duncan Jones

有很多使用Reset的充分理由,在这种情况下,我不会过多关注该Mockito引用。您可以在运行测试套件时让Spring的JUnit Class Runner引起不必要的交互,尤其是在进行涉及模拟数据库调用或涉及不需要使用反射的私有方法的测试时。
桑迪·西蒙顿

当我想测试多件事情时,通常会发现这很困难,但是JUnit根本没有提供任何好的(!)参数化测试的方法。与NUnit不同,例如带有注释。
Stefan Hendriks

3

绝对不。通常,编写干净测试的困难是有关生产代码设计的主要危险信号。在这种情况下,最好的解决方案是重构代码,以使Bar的构造函数不调用任何方法。

构造函数应该构造而不是执行逻辑。取得方法的返回值,并将其作为构造函数参数传递。

new Bar(mockedObject);

变成:

new Bar(mockedObject.someMethod());

如果这会导致在许多地方重复此逻辑,请考虑创建一个工厂方法,该方法可以独立于Bar对象进行测试:

public Bar createBar(MockedObject mockedObject) {
    Object dependency = mockedObject.someMethod();
    // ...more logic that used to be in Bar constructor
    return new Bar(dependency);
}

如果这种重构太困难了,那么使用reset()是一个很好的解决方法。但是我们要弄清楚-这表明您的代码设计不良。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.