匹配 Mockito 2 存根中的 varargs



如何在 Mockito 中正确匹配 varargs 回答如何匹配任何 vararg(包括在 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);
}
}
}

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

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

然后,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 完成参数匹配后 - 并且此匹配将为真 - 然后调用传递给thenAnswer的 lambda,如果找到给定元素,则返回 7,否则返回 0。

要记住的两件事:

  1. 对于每个测试的调用,您始终需要一个新ArrayElementMatcher- 或向类添加重置方法。

  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 稍微复杂一些,但它的使用要简单得多。

事实证明,我们有测试,它们存根对一种方法的多次调用,而且它们还匹配除 varargs 之外的其他参数。考虑到@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;
}
};
}
}
}

最新更新