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。
答案 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。
请记住两点:
对于每个经过测试的调用,您总是需要一个新的ArrayElementMatcher
-或在类中添加一个reset方法。
在具有不同匹配项的一种测试方法中,您不能有多个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;
}
};
}
}
}