3

How to properly match varargs in Mockito answers how to match any varargs (including in Mockito 2) and how to match more precisely (e.g. using Hamcrest matchers, but in Mockito 1). I need the latter in Mockito 2. Is that possible?

In this test, the test using any passes, but the one with the ArgumentMatcher fails (using 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);
        }
    }
}

BTW, class ArrayContainsMatcher is supposed to be inlined as anonymous class or lambda inside method arrayContains if implementing VarargMatcher is not necessary.

EndlosSchleife
  • 515
  • 7
  • 19

2 Answers2

1

When a method on a mock with vararg arguments is called, Mockito checks if the last matcher that was passed in to the when method is an ArgumentMatcher that implements the VarargMatcher interface. This is correct in your case.

Mockito then internally expands the list of matchers for the call by repeating this last matcher for every vararg argument so that in the end the internal list of arguments and the list of matchers have the same size. In your example this means that during the matching there are three arguments - "a", "b", "c" - and three matchers - three times the instance of the ArrayContainsMatcher.

Then Mockito tries to match each argument against the matcher. And here your code fails, because the argument is a String and the matcher needs a String[]. So the match fails and the mock returns the default value of 0.

So the important thing is that a VarargMatcher is not called with the array of vararg arguments, but repeatedly with every single argument.

To get the behaviour as you want it, you must implement a matcher that has internal state, and instead of using then to return a fixed value you need thenAnswer with code that evaluates the state.

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;
        }
    }
}

The ArrayElementMatcher always returns true for a single match, otherwise Mockito would abort the evaluation, but internally the information is stored if the desired element was encountered. When Mockito has finished matching the arguments - and this match will be true - then the lambda passed into thenAnswer is called and this returns 7 if the given element was found, or 0 otherwise.

Two things to keep in mind:

  1. you always need a new ArrayElementMatcher for every tested call - or add a reset method to the class.

  2. you cannot have more than one when(c.f((argThat(matcher))) defintions in one test method with different matchers, because only one of them would be evaluated.

Edit/Addition:

Just played around a little more and came up with this variation - just showing the Matcher class and the test method:

@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;
    }
}

It's basically the same, but I moved the implementation of the Answer interface into the matcher and extracted the logic to compare the vararg elements into a lambda that is passed in to the matcher ("b"::equals").

That makes the Matcher a little more complex, but the usage of it is much simpler.

P.J.Meisch
  • 18,013
  • 6
  • 50
  • 66
  • Thanks. Great explanation. If that `ArgumentMatcher` does not have full control however (leaving part of the responsibilty for the `thenAnswer`), I find it clearer to turn it into a `CapturingVarArgsTrueMatcher` with a method `public List getArgs()` and no need to know / compare the element. Then the test uses it as `when(c.f(argThat(matcher))) .thenAnswer(invocationOnMock -> matcher.getArgs().contains("b") ? 7 : 0);`. – EndlosSchleife Aug 27 '18 at 13:54
  • another interesting approach. it has more flexibility as you can test for example both anyMatch and allMatch with the same matcher. – P.J.Meisch Aug 27 '18 at 17:01
0

It turned out that we have tests which stub multiple invocations of one method, plus they also match other args besides the varargs. Considering @P.J.Meisch's warning that all those cases belong into a single then, I switched to the following alternative solution:

Each case is specified as an object (InvocationMapping) which matches an argument list an provides an Answer. All those are passed to a utility method which implements the single 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;
                }
            };
        }
    }
}
EndlosSchleife
  • 515
  • 7
  • 19