简体   繁体   English

如何在不使Mockito进行额外调用的情况下使用Mockito检查对函数的调用次数

[英]How to use Mockito to check number of calls to a function without making Mockito make an extra call

We have made our own framework that makes it easy to setup an analysis pipeline. 我们已经建立了自己的框架,可以轻松建立分析管道。 Every time an analysis ends, finish() is called. 每次分析结束时,都会调用finish()。 finish() uploads files that were generated during the analysis. finish()上传分析期间生成的文件。 To ensure that the framework was used correctly, we have made a check that finish() is not called twice. 为了确保正确使用框架,我们进行了一次检查,以确保未两次调用finish()。

Now, I want to test that finish() is called for a specific step in the pipeline. 现在,我想测试针对管道中的特定步骤调用了finish()。 I do this by calling the following in my test: 为此,我在测试中调用了以下命令:

verify(consumer).finish();

But apparently, verify() also calls finish() so an exception is thrown and the test fails. 但是显然,verify()也调用finish(),因此引发了异常并且测试失败。

Now, my question is: 现在,我的问题是:

  • How do I avoid that finish() is called twice? 如何避免两次调用finish()?

EDIT 编辑

A quick setup of the problem: 问题的快速设置:

Analysis 分析

package mockitoTwice;

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

Finishable 最终处理

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();
}

Pipeline 管道

package mockitoTwice;

public class Pipeline {
    private Analysis analysis;

    public Pipeline(Analysis analysis) {
        this.analysis = analysis;
    }

    public void runAnalyses() {
        analysis.finish();
    }
}

PipelineTest PipelineTest

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();
    }
}

Testing frameworks all have their quirks, but when you encounter problems like this then the first step is to assess your class and test design. 测试框架都有其独特之处,但是当您遇到此类问题时,第一步就是评估您的课程和测试设计。

First, I notice that AnalysisTest isn't really testing the Analysis class. 首先,我注意到AnalysisTest并未真正测试Analysis类。 It's mocking Analysis and actually testing the Pipeline class. 它是在模拟Analysis并实际测试Pipeline类。 A proper test for Analysis would look something like this: 正确的Analysis测试如下所示:

@Test
public void testFinishTwice() {

    Analysis analysis = new Analysis();

    try {
        analysis.finish();
        analysis.finish();
        Assert.fail();
    } catch (IllegalStateException ex) {
       // Optionally assert something about ex
    }
}

This will verify the implied contract that Analysis throws IllegalStateException when finish() is called more than once. 这将验证当多次调用finish()时Analysis抛出IllegalStateException的隐含约定。 There are a variety of solutions to your problem and most of them depend on validating this. 有多种解决方案可以解决您的问题,其中大多数都取决于对此的验证。

Next, the abstract Finishable class with a final finish() method is not quite as foolproof as it looks. 接下来,带有final finish()方法的抽象Finishable类看起来并不十分安全。 Since the finishHelper method has protected access, then it is still accessible directly to any class in the package. 由于finishHelper方法具有受保护的访问权限,因此包中的任何类仍可以直接访问该方法。 So in your example, if Pipeline and Analysis are in the same package, then Pipeline could call finishHelper directly. 因此,在您的示例中,如果Pipeline和Analysis位于同一软件包中,则Pipeline可以直接调用finishHelper。 I would guess that's the single biggest risk of the actual finish code getting called twice. 我想这是实际完成代码被调用两次的最大风险。 How easy would it be to accidentally let your IDE autocomplete to finishHelper? 意外让您的IDE自动完成完成finishHelper有多容易? Even if your unit test worked as you wanted, it could not catch this. 即使您的单元测试按照您的要求进行,它也无法抓住这一点。

Now that I've addressed that, we can get to the root of the problem. 现在,我已经解决了这个问题,我们可以找到问题的根源。 The finish method is marked as final, so Mockito can't override it. finish方法标记为final,因此Mockito无法覆盖它。 Normally, Mockito would create a stub method for it but here it has to use the original code from Finishable. 通常,Mockito会为其创建一个存根方法,但是在这里它必须使用Finishable中的原始代码。 A true mock wouldn't even print "Calling finish()" when finish is called. 当调用finish时,真正的模拟程序甚至都不会打印“ Calling finish()”。 Since it's stuck with original implementation, the real finish method gets called both by pipeline and then again by verify(analysis).finish(); 由于它停留在原始实现上,因此真正的finish方法既可以通过管道调用,也可以通过verify(analysis).finish()再次调用。

So what do we do about it? 那么我们该怎么办? There's no perfect answer and it really depends on details of the situation. 没有完美的答案,这实际上取决于具体情况。

The simplest approach would be to drop the final keyword on the finish method. 最简单的方法是将final关键字放在finish方法上。 Then you just have to make sure that Analysis and Pipeline are not in the same package. 然后,只需确保Analysis和Pipeline不在同一程序包中。 The test you wrote ensures and the Pipeline only calls finish once. 您编写的测试确保了“仅管道”调用完成一次。 The test I suggested guarantees an exception the second time finish is called on Analysis. 我建议的测试可确保在Analysis上调用第二次完成的异常。 This happens even if it would override finish. 即使会覆盖完成,也会发生这种情况。 You could still abuse it, but you'd have to deliberately go out of your way to do it. 您仍然可以滥用它,但是您必须刻意去做。

You could also switch Finishable to an interface and rename your current class AbstractFinishable as a base implementation. 您也可以将Finishable切换为接口,并将当前类AbstractFinishable重命名为基本实现。 Then switch Analysis to an interface that extends Finishable and create an ExampleAnalysis class that extends AbstractFinishable and implements Analysis. 然后将Analysis切换到扩展Finishable的接口,并创建一个扩展AbstractFinishable并实现Analysis的ExampleAnalysis类。 Then Pipeline references the Analysis interface. 然后,管道引用分析接口。 We have to do it that way because otherwise it could get access to finishHelper and we are back where we started. 我们必须这样做,因为否则它可以访问finishHelper,我们回到了起点。 Here's a sketch of the code: 这是代码的草图:

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                                                                                                               
}

So that's one way to do it. 所以这是做到这一点的一种方法。 It's essentially switching the classes to be coded to interfaces of their dependencies rather than specific class implementations. 实质上是将要编码的类切换到其依赖项的接口,而不是特定的类实现。 That's generally easier to mock and test. 通常,这更容易模拟和测试。 You could also use the delegate pattern and just put a Finishable on ExampleAnalysis rather than extending AbstractFinishable. 您还可以使用委托模式,仅在ExampleAnalysis上放置Finishable,而不是扩展AbstractFinishable。 There are other ways, too, and these are just ideas. 也有其他方法,这些只是想法。 You should know the specifics of your project well enough to decide the best route. 您应该充分了解项目的细节,以决定最佳路线。

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

The problem can be solved by catching the exception of the framework as follows: 可以通过捕获框架异常来解决此问题,如下所示:

@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();
}

If finish() is called too few times, the verify handles the problem as one would expect. 如果finish()的调用次数太少,则验证会按预期处理该问题。

If finish() is called too many times, the exception is called on pipeline.runAnalyses() . 如果finish()被调用太多,则在pipeline.runAnalyses()上调用该异常。

Otherwise, the test is successful. 否则,测试成功。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM