матч varargs в Mockito 2 заглушки - PullRequest
0 голосов
/ 24 августа 2018

Как правильно сопоставить varargs в 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 не требуется.

Ответы [ 2 ]

0 голосов
/ 30 августа 2018

Оказалось, что у нас есть тесты, которые блокируют множественные вызовы одного метода, плюс они также соответствуют другим аргументам, кроме 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;
                }
            };
        }
    }
}
0 голосов
/ 25 августа 2018

Когда вызывается метод для макета с аргументами 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 в противном случае.

Две вещи, которые следует сохранитьпомните:

  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 Интерфейс в matcher и извлек логику для сравнения элементов vararg в лямбду, которая передается в matcher ("b"::equals").

Это делает Matcher немного более сложным, но использование егоявляется намного проще.

...