匹配Mockito 2捣蛋的varargs

问题描述 投票:2回答:2

How to properly match varargs in Mockito回答如何匹配任何varargs(包括在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。

java mockito variadic-functions
2个回答
1
投票

当调用带有vararg参数的mock的方法时,Mockito会检查传递给when方法的最后一个匹配器是否是实现ArgumentMatcher接口的VarargMatcher。这在你的情况下是正确的。

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

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

所以重要的是,不使用vararg参数数组调用VarargMatcher,而是使用每个参数重复调用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总是为单个匹配返回thenAnswer,否则Mockito将中止评估,但如果遇到所需元素,则在内部存储信息。当Mockito完成匹配参数 - 并且这个匹配将为真 - 然后调用传入ArrayElementMatcher的lambda,如果找到给定元素则返回7,否则返回0。

要记住两件事:

  1. 你总是需要为每次测试调用一个新的when(c.f((argThat(matcher))) - 或者在课堂上添加一个重置方法。
  2. 在一种具有不同匹配器的测试方法中,您不能有多个qazxsw poi定义,因为只会评估其中一个。

编辑/添加:

只是玩了一下,然后想出了这个变化 - 只显示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元素比较为传递给匹配器("b"::equals")的lambda。

这使得Matcher更加复杂,但使用它更加简单。


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;
                }
            };
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.