How to Properly Match Varargs in Mockito

How to properly match varargs in Mockito

Mockito 1.8.1 introduced anyVararg() matcher:

when(a.b(anyInt(), anyInt(), Matchers.<String>anyVararg())).thenReturn(b);

Also see history for this: https://code.google.com/archive/p/mockito/issues/62

Edit new syntax after deprecation:

when(a.b(anyInt(), anyInt(), ArgumentMatchers.<String>any())).thenReturn(b);

Matching varargs in Mockito for unit testing in an overloaded method

The following works for me:

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.web.client.RestTemplate;

@RunWith(MockitoJUnitRunner.class)
public class DemoApplicationTests {
@Mock
private RestTemplate restTemplate;
@Test
public void testy() {
Mockito.when(restTemplate.getForObject(Mockito.anyString(), Mockito.any(), (Object) Mockito.any())).thenReturn("this works");
Assert.assertEquals("this works", restTemplate.getForObject("hi", String.class, 42, 45));
}

}

match varargs in Mockito 2 stubbing

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.

How to stub varargs in Mockito to match only one argument

You have to stub your method 2 times. First the least specific stub:

val foo = Foo()
whenever(foo.bar(any())).thenReturn(false) // or whatever you want to return/throw here

And then the more specific single argument method:

whenever(foo.bar(eq("AAA"))).thenReturn(true)

After your comment you may aswell use something like this (using Java this time):

when(foo.bar(any())).thenAnswer(invocation -> {
for (Object argument : invocation.getArguments()) {
if ("AAA".equals(argument)) return true;
}
return false;
});

And the same in Kotlin

whenever(foo.bar(any()).thenAnswer {
it.arguments.contains("AAA")
}

Mockito when clause with typed vararg parameter - Unit test using anyVararg

anyVararg is a parameterized method. Try:

Alpha alphaMock = Mockito.mock(Alpha.class);
when(alphaMock.test(Matchers.<String> anyVararg())).thenReturn("success");

is there Mockito eq matcher for varargs array?

Ok, I think the answer here requires a custom built matcher, which can be implemented in your unit test as so:

private class MyVarargMatcher extends ArgumentMatcher<Object[]> implements VarargMatcher {
private Object[] expectedValues;

MyVarargMatcher(Object... expectedValues) {
this.expectedValues = expectedValues;
}

@Override
public boolean matches(Object varargArgument) {
return new EqualsBuilder()
.append(expectedValues, varargArgument)
.isEquals();
}
}

Then, in testMakeList1() change the first line to this:

Mockito.doReturn(expected).when(target).toList1(Mockito.argThat(new MyVarargMatcher(objectArray)));

Sources:

How to properly match varargs in Mockito

http://maciejmadej.blogspot.com/2011/11/capturing-varargs-argument-using-custom.html



Related Topics



Leave a reply



Submit