Когда вызывается метод для макета с аргументами 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 завершил сопоставление аргументов - и это совпадение будет истинным - тогда вызывается лямбда, переданная в thenAnswer
, и возвращает 7, если данный элемент был найден, или 0 в противном случае.
Две вещи, которые следует сохранитьпомните:
вам всегда нужен новый ArrayElementMatcher
для каждого проверенного вызова - или добавьте метод сброса в класс.
выне может иметь более одного 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
Интерфейс в matcher и извлек логику для сравнения элементов vararg в лямбду, которая передается в matcher ("b"::equals"
).
Это делает Matcher немного более сложным, но использование егоявляется намного проще.