EasyMock:如何在集合顺序无关紧要的情况下验证值集合的方法顺序

EasyMock: How to Verify Method Order for Set of Values Where Order of Set Does Not Matter

我有一个测试,其中我有一组特定值,两个不同的方法将针对该组中的每个值执行一次。我需要检查这两种方法是否以相对于彼此的特定顺序调用,但与值集的顺序无关。例如:

String[] values = { "A", "B", "C" };

for (...<loop over values...) {
    methodOne(value);
    methodTwo(value);
}

values 的顺序无关紧要,但我需要验证是否为集合中的每个值调用了 methodOne()methodTwo() 并且 methodOne() 总是在 methodTwo().

之前调用

我知道我可以创建一个控件并为每个值期望 methodOne()methodTwo(),然后执行 control.verify(),但这取决于 values 在具体顺序。

有没有优雅的方法来做到这一点?

谢谢

您可以使用 andAnswer() 来做到这一点。

基本上,在 methodOne()andAnswer() 中,您设置了一些变量来保存传入 value 的内容。

然后在 methodTwo()andAnswer() 中,您断言相同的参数与您从 methodOne 答案中保存的内容相匹配。

由于每次调用 methodOne 都会修改此变量,因此会确保 methodTwo() 始终在 methodOne() 之后调用。

注意这个解决方案不是线程安全的

首先,您需要一些东西来保存 methodOne 调用中的变量。这可以是一个简单的 class,具有单个字段,甚至是一个元素的数组。您需要此包装器对象,因为您需要在需要最终或有效最终字段的 IAnswer 中引用它。

private class CurrentValue{
    private String methodOneArg;

}

现在您的期望。这里我调用了你正在测试的 class (The System Under Test) sut:

    String[] values = new String[]{"A", "B", "C"};

    final CurrentValue currentValue = new CurrentValue();

    sut.methodOne(isA(String.class));

    expectLastCall().andAnswer(new IAnswer<Void>() {

        @Override
        public Void answer() throws Throwable {
            //save the parameter passed in to our holder object
            currentValue.methodOneArg =(String) EasyMock.getCurrentArguments()[0];
            return null;
        }

    }).times(values.length); // do this once for every element in values

    sut.methodTwo(isA(String.class));
    expectLastCall().andAnswer(new IAnswer<Void>() {

        @Override
        public Void answer() throws Throwable {
            String value =(String) EasyMock.getCurrentArguments()[0];
            //check to make sure the parameter matches the 
            //the most recent call to methodOne()

            assertEquals(currentValue.methodOneArg, value);
            return null;
        }

    }).times(values.length); // do this once for every element in values

    replay(sut);
    ... //do your test
    verify(sut);

编辑

你是对的,如果你使用 EasyMock 2.4 +,你可以使用新的 Capture class 以更清晰的方式为 methodOne() 获取参数值。但是,您可能仍需要为 methodTwo() 使用 andAnswer() 以确保按顺序调用正确的值。

这是使用 Capture 的相同代码

Capture<String> captureArg = new Capture<>();

    sut.methodOne(and(capture(captureArg), isA(String.class)));
    expectLastCall().times(values.length);

    sut.methodTwo(isA(String.class));
    expectLastCall().andAnswer(new IAnswer<Void>() {

        @Override
        public Void answer() throws Throwable {
            String value =(String) EasyMock.getCurrentArguments()[0];
            assertEquals(captureArg.getValue(), value);
            return null;
        }

    }).times(values.length);

    replay(sut);

对于那些感兴趣的人,我使用预期的 EasyMock 功能解决了这个问题。解决方案是制作一个自定义 IArgumentMatcher 来验证一组值并强制每个值连续匹配的次数。自定义匹配器,除了使用严格的模拟之外,还完全解决了原来的问题。

public class SetMatcher implements IArgumentMatcher {

    private List<String> valuesToMatch;
    private List<String> remainingValues;
    private String currentValue = null;
    private int timesMatched = 0;
    private int setMatches;

    public SetMatcher(final List<String> valuesToMatch, final int times) {
        this.valuesToMatch = new ArrayList<String>(valuesToMatch);
        this.remainingValues = new ArrayList<String>(valuesToMatch);
        this.setMatches = times;
    }

    public String use() {
        EasyMock.reportMatcher(this);

        return null;
    }

    public void appendTo(StringBuffer buffer) {
        if (this.remainingValues.size() == 0) {
            buffer.append("all values in " + this.valuesToMatch + " already matched " + this.setMatches + " time(s)");
        } else {
            buffer.append("match " + this.valuesToMatch + " " + this.setMatches + " time(s) each");
        }
    }

    public boolean matches(Object other) {

        if (this.timesMatched >= this.setMatches) {
            this.currentValue = null;
            this.timesMatched = 0;
        }

        if (null == this.currentValue) {
            if (this.remainingValues.contains(other)) {
                this.currentValue = (String) other;
                this.timesMatched = 1;
                this.remainingValues.remove(other);

                return true;
            }
        } else if (this.currentValue.equals(other)) {
            this.timesMatched++;

            return true;
        }

        return false;
    }

}

正在测试的class:

public class DataProcessor {
    private ServiceOne serviceOne;
    private ServiceTwo serviceTwo;

    public DataProcessor(ServiceOne serviceOne, ServiceTwo serviceTwo) {
        this.serviceOne = serviceOne;
        this.serviceTwo = serviceTwo;
    }

    public void processAll(List<String> allValues) {
        List<String> copy = new ArrayList<String>(allValues);
        for (String value : copy) {
            this.serviceOne.preProcessData(value);
            this.serviceTwo.completeTransaction(value);
        }
    }
}

测试:

public class DataProcessorTest {

    List<String> TEST_VALUES = Arrays.asList("One", "Two", "Three", "Four", "Five");

    @Test
    public void test() {
        IMocksControl control = EasyMock.createStrictControl();
        ServiceOne serviceOne = control.createMock(ServiceOne.class);
        ServiceTwo serviceTwo = control.createMock(ServiceTwo.class);

        SetMatcher matcher = new SetMatcher(TEST_VALUES, 2);

        for (int i = 0; i < TEST_VALUES.size(); i++) {
            serviceOne.preProcessData(matcher.use());
            serviceTwo.completeTransaction(matcher.use());
        }

        control.replay();

        DataProcessor dataProcessor = new DataProcessor(serviceOne, serviceTwo);
        dataProcessor.processAll(TEST_VALUES);

        control.verify();
    }
}

测试将因以下任何一项而失败:

  • ServiceOne 和 ServiceTwo 的调用顺序错误
  • ServiceOne 和 ServiceTwo 没有连续调用相同的值
  • 使用不在指定值列表中的值调用了 ServiceOne 或 ServiceTwo
  • 对列表中的某个值进行了超出预期次数的调用