在Mockito 2存根中匹配varargs

时间:2018-08-24 13:41:30

标签: java mockito variadic-functions

How to properly match varargs in Mockito回答了如何匹配任何可变参数(包括在Mockito 2中)以及如何更精确地匹配(例如,使用Hamcrest匹配器,但在Mockito 1中)。我需要在Mockito 2中使用后者。这可能吗?

在此测试中,使用any的测试通过了,但是使用ArgumentMatcher的测试失败了(使用org.mockito:mockito-core:2.15.0):

package test.mockito;

import java.io.Serializable;
import java.util.Arrays;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import org.mockito.ArgumentMatcher;
import static org.mockito.Mockito.*;
import org.mockito.internal.matchers.VarargMatcher;

public class TestVarArgMatcher {
    interface Collaborator {
        int f(String... args);
    }

    @Test
    public void testAnyVarArg() {
        Collaborator c = mock(Collaborator.class);
        when(c.f(any())).thenReturn(6);
        assertEquals(6, c.f("a", "b", "c")); // passes
    }

    @Test
    public void testVarArg() {
        Collaborator c = mock(Collaborator.class);
        when(c.f(argThat(arrayContains("b")))).thenReturn(7);
        assertEquals(7, c.f("a", "b", "c")); // fails: expected:<7> but was:<0>
    }

    static <T extends Serializable> ArgumentMatcher<T[]> arrayContains(T element) {
        return new ArrayContainsMatcher<>(element);
    }

    private static class ArrayContainsMatcher<T> implements ArgumentMatcher<T[]>, VarargMatcher {
        private static final long serialVersionUID = 1L;
        private final T element;

        public ArrayContainsMatcher(T element) {
            this.element = element;
        }

        @Override
        public boolean matches(T[] array) {
            return Arrays.asList(array).contains(element);
        }
    }
}

顺便说一句,如果不需要实现ArrayContainsMatcher,应该将类arrayContains内联为方法VarargMatcher中的匿名类或lambda。

2 个答案:

答案 0 :(得分:1)

当调用带有vararg参数的模拟方法时,Mockito将检查传递给when方法的最后一个匹配器是否为实现ArgumentMatcher接口的VarargMatcher。这对您来说是正确的。

然后,Mockito通过对每个vararg参数重复最后一个匹配器,在内部扩展该调用的匹配器列表,以便最后内部参数列表和匹配器列表具有相同的大小。在您的示例中,这意味着在匹配期间,存在三个参数-“ a”,“ b”,“ c”-和三个匹配器-是ArrayContainsMatcher实例的三倍。

然后,Mockito尝试匹配匹配器的每个参数。这里的代码失败了,因为参数是String,匹配器需要String[]。因此,匹配失败,并且模拟返回默认值0。

所以重要的是,VarargMatcher不会与vararg参数数组一起调用,而是与每个参数一起重复。

要获得所需的行为,必须实现一个具有内部状态的匹配器,而不是使用then返回一个固定值,而需要thenAnswer及其用于评估状态的代码。

import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.internal.matchers.VarargMatcher;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class TestVarArgMatcher {

    @Test
    public void testAnyVarArg() {
        Collaborator c = mock(Collaborator.class);
        when(c.f(any())).thenReturn(6);
        assertEquals(6, c.f("a", "b", "c")); // passes
    }

    @Test
    public void testVarArg() {
        Collaborator c = mock(Collaborator.class);

        ArrayElementMatcher<String> matcher = new ArrayElementMatcher<>("b");
        when(c.f(argThat(matcher))).thenAnswer(invocationOnMock -> matcher.isElementFound() ? 7 : 0);

        assertEquals(7, c.f("a", "b", "c")); 
    }


    interface Collaborator {
        int f(String... args);
    }

    private static class ArrayElementMatcher<T> implements ArgumentMatcher<T>, VarargMatcher {
        private final T element;
        private boolean elementFound = false;

        public ArrayElementMatcher(T element) {
            this.element = element;
        }

        public boolean isElementFound() {
            return elementFound;
        }

        @Override
        public boolean matches(T t) {
            elementFound |= element.equals(t);
            return true;
        }
    }
}

ArrayElementMatcher始终为单个匹配返回true,否则Mockito将中止评估,但是如果遇到所需的元素,则在内部存储信息。当Mockito完成对参数的匹配后(此匹配为true),则调用传递到thenAnswer的lambda,如果找到给定元素,则返回7,否则返回0。

请记住两点:

  1. 对于每个经过测试的调用,您总是需要一个新的ArrayElementMatcher-或在类中添加一个reset方法。

  2. 在具有不同匹配项的一种测试方法中,您不能有多个when(c.f((argThat(matcher)))定义,因为只有其中一个会被评估。

编辑/添加:

只是玩了一点,并想出了这个变化-仅显示Matcher类和测试方法:

@Test
public void testVarAnyArg() {
    Collaborator c = mock(Collaborator.class);

    VarargAnyMatcher<String, Integer> matcher = 
            new VarargAnyMatcher<>("b"::equals, 7, 0);
    when(c.f(argThat(matcher))).thenAnswer(matcher);

    assertEquals(7, c.f("a", "b", "c"));
}

private static class VarargAnyMatcher<T, R> implements ArgumentMatcher<T>, VarargMatcher, Answer<R> {
    private final Function<T, Boolean> match;
    private final R success;
    private final R failure;
    private boolean anyMatched = false;

    public VarargAnyMatcher(Function<T, Boolean> match, R success, R failure) {
        this.match = match;
        this.success = success;
        this.failure = failure;
    }

    @Override
    public boolean matches(T t) {
        anyMatched |= match.apply(t);
        return true;
    }

    @Override
    public R answer(InvocationOnMock invocationOnMock) {
        return anyMatched ? success : failure;
    }
}

基本上是相同的,但是我将Answer接口的实现移到了匹配器中,并提取了逻辑以将vararg元素比较成一个lambda,然后将其传递给匹配器("b"::equals"

这使Matcher稍微复杂一些,但使用起来却简单得多。

答案 1 :(得分:0)

事实证明,我们有测试可以对一种方法的多次调用进行存根,而且它们还与除varargs之外的其他args匹配。考虑到@ P.J.Meisch的警告,即所有这些情况都属于一个then,因此我切换到以下替代解决方案:

每种情况都指定为与参数列表匹配并提供InvocationMapping的对象(Answer)。所有这些都传递给实现单个then的实用程序方法。

package test.mockito;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.mockito.invocation.Invocation;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

public class TestVarArgMatcher2 {
    interface Collaborator {
        int f(int i, Character c, String... args);
    }

    @Test
    public void test() {
        Collaborator c = mock(Collaborator.class);

        TestUtil.strictWhenThen(c.f(anyInt(), any(), any()),
                InvocationMapping.match(i -> 6, ((Integer) 11)::equals, arg -> Character.isDigit((Character) arg), arg -> Arrays.asList((Object[]) arg).contains("b")),
                InvocationMapping.match(i -> 7, ((Integer) 12)::equals, arg -> Character.isJavaIdentifierPart((Character) arg), arg -> Arrays.asList((Object[]) arg).contains("b")));

        assertEquals(6, c.f(11, '5', "a", "b")); // passes
        assertEquals(7, c.f(12, 'j', "b")); // passes
        assertEquals(7, c.f(12, 'j', "a", "c")); // fails with "no behavior defined..." (as desired)
    }

    public static class TestUtil {
        @SafeVarargs
        public static <T> void strictWhenThen(T whenAny, InvocationMapping<T>... invocationMappings) {
            whenThen(whenAny, i -> {
                throw new IllegalStateException("no behavior defined for invocation on mock: " + i);
            }, invocationMappings);
        }

        @SafeVarargs
        public static <T> void whenThen(T whenAny, Answer<? extends T> defaultAnswer, InvocationMapping<T>... invocationMappings) {
            when(whenAny).then(invocation -> {
                for (InvocationMapping<T> invocationMapping : invocationMappings) {
                    if (invocationMapping.matches(invocation)) {
                        return invocationMapping.getAnswer(invocation).answer(invocation);
                    }
                }
                return defaultAnswer.answer(invocation);
            });
        }
    }

    public interface InvocationMapping<T> {
        default boolean matches(InvocationOnMock invocation) { return getAnswer(invocation) != null; }

        Answer<T> getAnswer(InvocationOnMock invocation);

        /** An InvocationMapping which checks all arguments for equality. */
        static <T> InvocationMapping<T> eq(Answer<T> answer, Object... args) {
            return new InvocationMapping<T>() {
                @Override
                public boolean matches(InvocationOnMock invocation) {
                    Object[] invocationArgs = ((Invocation) invocation).getRawArguments();
                    return Arrays.asList(args).equals(Arrays.asList(invocationArgs));
                }

                @Override
                public Answer<T> getAnswer(InvocationOnMock invocation) {
                    if (!matches(invocation)) {
                        throw new IllegalArgumentException("invocation " + invocation + " does not match " + Arrays.toString(args));
                    }
                    return answer;
                }
            };
        }

        /** An InvocationMapping which checks all arguments using the given matchers. */
        @SafeVarargs
        static <T> InvocationMapping<T> match(Answer<T> answer, ArgumentMatcher<Object>... matchers) {
            return new InvocationMapping<T>() {
                @Override
                public boolean matches(InvocationOnMock invocation) {
                    Object[] args = ((Invocation) invocation).getRawArguments();
                    if (matchers.length != args.length) {
                        return false;
                    }
                    for (int i = 0; i < args.length; i++) {
                        if (!matchers[i].matches(args[i])) {
                            return false;
                        }
                    }
                    return true;
                }

                @Override
                public Answer<T> getAnswer(InvocationOnMock invocation) {
                    if (!matches(invocation)) {
                        throw new IllegalArgumentException("invocation " + invocation + " does not match " + Arrays.toString(matchers));
                    }
                    return answer;
                }
            };
        }
    }
}