如何使用 Mockito 检查对函数的调用次数,而无需让 Mockito 进行额外的调用



我们制作了自己的框架,可以轻松设置分析管道。 每次分析结束时,都会调用 finish()。 finish() 上传在分析过程中生成的文件。 为了确保正确使用框架,我们检查了 finish() 是否未被调用两次。

现在,我想测试为管道中的特定步骤调用 finish()。 我通过在测试中调用以下内容来执行此操作:

verify(consumer).finish();

但显然,verify() 也调用 finish(),因此会抛出异常并且测试失败。

现在,我的问题是:

  • 如何避免finish() 被调用两次?

编辑

问题的快速设置:

分析

package mockitoTwice;
public class Analysis extends Finishable {
@Override
public void finishHelper() {
System.out.println("Calling finishHelper()");
}
}

可完成

package mockitoTwice;
public abstract class Finishable {
protected boolean finished = false;
public final void finish() {
System.out.println("Calling finish()");
if (finished) {
throw new IllegalStateException();
}
finished = true;
finishHelper();
}
public abstract void finishHelper();
}

管道

package mockitoTwice;
public class Pipeline {
private Analysis analysis;
public Pipeline(Analysis analysis) {
this.analysis = analysis;
}
public void runAnalyses() {
analysis.finish();
}
}

管道测试

package mockitoTwice;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.Test;
public class PipelineTest {
@Test
public void test() {
Analysis analysis = mock(Analysis.class);
Pipeline pipeline = new Pipeline(analysis);
pipeline.runAnalyses();
verify(analysis).finish();
}
}

测试框架都有其怪癖,但是当你遇到这样的问题时,第一步就是评估你的类和测试设计。

首先,我注意到 AnalysisTest 并没有真正测试 Analysis 类。它模拟分析并实际测试管道类。正确的分析测试如下所示:

@Test
public void testFinishTwice() {
Analysis analysis = new Analysis();
try {
analysis.finish();
analysis.finish();
Assert.fail();
} catch (IllegalStateException ex) {
// Optionally assert something about ex
}
}

这将验证 Analysis 在多次调用 finish() 时抛出 IllegalStateException 的隐含合约。您的问题有多种解决方案,其中大多数都取决于对此的验证。

接下来,带有最终 finish() 方法的抽象 Finishable 类并不像看起来那么万无一失。由于 finishHelper 方法具有受保护的访问权限,因此包中的任何类仍可直接访问它。因此,在您的示例中,如果管道和分析位于同一个包中,则管道可以直接调用 finishHelper。我猜这是实际完成代码被调用两次的最大风险。意外地让您的 IDE 自动完成以完成助手有多容易?即使你的单元测试按你想要的方式工作,它也无法捕捉到这一点。

现在我已经解决了这个问题,我们可以找到问题的根源了。完成方法被标记为最终方法,因此 Mockito 无法覆盖它。通常,Mockito会为它创建一个存根方法,但在这里它必须使用Finishable中的原始代码。真正的模拟甚至不会在调用完成时打印"调用完成()"。由于它停留在原始实现中,因此真正的 finish 方法既由管道调用,又由 verify(analysis).finish() 调用;

那么我们该怎么做呢?没有完美的答案,这实际上取决于情况的细节。

最简单的方法是在 finish 方法上删除 final 关键字。然后,您只需要确保分析和管道不在同一包中。你编写的测试可确保管道调用仅完成一次。我建议的测试保证了在分析中第二次调用完成时出现异常。即使它会覆盖完成,也会发生这种情况。你仍然可以滥用它,但你必须故意不遗余力地去做。

您还可以将 Finishable 切换到接口,并将当前类 AbstractFinishable 重命名为基本实现。然后将"分析"切换到扩展"可完成"的接口,并创建一个扩展"抽象可完成"并实现"分析"的示例分析类。然后,管道引用分析接口。我们必须这样做,因为否则它可以访问 finishHelper,我们又回到了起点。下面是代码的草图:

public interface Finishable {
public void finish();
}
public abstract class AbstractFinishable implements Finishable {
// your current Finishable class with final finish method goes here                                                                                                                   
}
public interface Analysis extends Finishable {
// Other analysis methods that Pipeline needs go here                                                                                                        
}
public ExampleAnalysis extends AbstractFinishable implements Analysis {
// Implementations of Analysis methods go here                                                                                                               
}

所以这是一种方法。它本质上是将要编码的类切换到其依赖项的接口,而不是特定的类实现。这通常更容易模拟和测试。您也可以使用委托模式,只需在ExampleAnalysis上放置一个Finishable,而不是扩展AbstractFinishable。还有其他方法,这些只是想法。您应该足够了解项目的具体情况,以决定最佳路线。

我像这样验证它:verify(object, times(1)).doStuff();

这个问题可以通过捕获框架的异常来解决,如下所示:

@Rule
public ExpectedException exception;
@Test
public void test() {
Analysis analysis = mock(Analysis.class);
Pipeline pipeline = new Pipeline(analysis);
pipeline.runAnalyses();
exception.expect(IllegalStateException.class);
verify(analysis).finish();
}

如果 finish() 被调用的次数太少,验证会像预期的那样处理问题。

如果 finish() 被调用的次数太多,则在pipeline.runAnalyses()上调用异常。

否则,测试成功。

相关内容

  • 没有找到相关文章

最新更新