我们制作了自己的框架,可以轻松设置分析管道。 每次分析结束时,都会调用 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()
上调用异常。
否则,测试成功。