Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Mockito handle overlapping matchers with multiple arguments in the thenReturn block

Tags:

java

mockito

I've got a block of test code that is attempting to, in the generic case return two values on subsequent calls, but in specific cases only return the value associated with that case. The code looks something like:

when(mockObject.method(anyString())).thenReturn(string1, string2);
when(mockObject.method(eq("expectedInput1"))).thenReturn(string1);
when(mockObject.method(eq("expectedInput2"))).thenReturn(string2);

The expected behavior is that when calling mockObject.method("foo") and mockObject.method("bar"), string1 and string2 should be returned respectively, but the test is actually seeing two responses of string2. Is this a bug in Mockito? Or am I misunderstanding Mockito pattern matching.

My assumption was that the last pattern to match is what is returned, but when going through that process does Mockito treat each argument in the first thenReturn block separately? Is there any way to get around this behavior?

When I comment out the second two when calls, the mocks behave as expected, so I'm assuming there is something specific about the overlapping matcher behavior.

Edit: This is in Mockito version 1.9.5

like image 673
Clayton Avatar asked Oct 25 '16 17:10

Clayton


3 Answers

I had this problem today. It is caused by calls to the mock to set up stubbing actually consuming the stubbing already in place.

In this example, change the first line to

when(mock.call(anyString())).thenReturn("","",string1,string2)

This will give you two blank responses when you set up your other mock returns, leaving string1 as the first useful return value.

Try also the doReturn alternative which I think may not have these issues:

doReturn(string1,string2).when(mock).call(anyString());

This uses the stub differently during setup.

So I did some more research on this. Here's the function I was playing with, based on the OP's question:

    Function<String, String> function = mock(Function.class);
    when(function.apply(anyString())).thenReturn("A","B","C");
    when(function.apply("Jim")).thenReturn("Jim");
    when(function.apply("Bob")).thenReturn("Bob");

    assertThat(function.apply("Jim")).isEqualTo("Jim");
    assertThat(function.apply("Bob")).isEqualTo("Bob");
    assertThat(function.apply("")).isEqualTo("A");
    assertThat(function.apply("")).isEqualTo("B");
    assertThat(function.apply("")).isEqualTo("C");
    assertThat(function.apply("")).isEqualTo("C");

The above fails at isEqualTo("A") because the two calls to set up the mocks for Jim and Bob consume return values from the list provided to anyString().

You might be tempted to reorder the when clauses, but that fails, because the anyString() supersedes the special cases, so that fails too.

The following version of the above DOES work as expected:

    when(function.apply(anyString())).thenReturn("A","B","C");

    doReturn("Jim")
        .when(function)
        .apply("Jim");
    doReturn("Bob")
        .when(function)
        .apply("Bob");

    assertThat(function.apply("Jim")).isEqualTo("Jim");
    assertThat(function.apply("Bob")).isEqualTo("Bob");
    assertThat(function.apply("")).isEqualTo("A");
    assertThat(function.apply("")).isEqualTo("B");
    assertThat(function.apply("")).isEqualTo("C");
    assertThat(function.apply("")).isEqualTo("C");

This is because the doReturn technique, which is intended for modifying pre-existing mocks in flight, doesn't actually involve calling the method on the mock to set up the mocking.

You could use doReturn for all setup, rather than mixing between when...thenReturn and doReturn..when..function(). As it happens, that's a bit uglier:

    doReturn("A").doReturn("B").doReturn("C")
        .when(function)
        .apply(anyString());

There's no convenient varargs function to let you specify multiple returns in sequence. The above has been tested and does work, though.

like image 151
Ashley Frieze Avatar answered Oct 19 '22 08:10

Ashley Frieze


It's hard to say whether that's a bug or a feature... The thing is, when you call mockObject.method(eq("expectedInput1")) to perform the second stubbing, the first stubbing is already in place. So this call returns string1, which is then uselessly passed to when. Subsequent calls return string2, and that includes the call for the last stubbing and later calls during actual testing.

I can hardly see any elegant way around it, short of using a custom Answer like @Nicolas suggested, although it does seem like an overkill. You could, perhaps, use a custom matcher instead of anyString(), that would essentially say “any string except those two”. This way you won't have one matcher intersect with another.

P. S. Now that @Nicolas edited his answer, that regular expression looks like exactly what I meant. Except that you don't need to implement a custom matcher after all.

like image 2
Sergei Tachenov Avatar answered Oct 19 '22 07:10

Sergei Tachenov


One way to workaround this problem could be to use a regular expression to avoid overlapping as next:

when(mockObject.method(eq("expectedInput1"))).thenReturn(string1);
when(mockObject.method(eq("expectedInput2"))).thenReturn(string2);
// Match with any input string that doesn't contain expectedInput1 neither expectedInput2
when(mockObject.method(matches("((?!expectedInput1|expectedInput2).)*")))
    .thenReturn(string1, string2);

Example:

System.out.println("expectedInput1=" + mockObject.method("expectedInput1"));
System.out.println("expectedInput2=" + mockObject.method("expectedInput2"));
System.out.println("foo=" + mockObject.method("foo"));
System.out.println("bar=" + mockObject.method("bar"));
System.out.println("bar=" + mockObject.method("bar"));

Output:

expectedInput1=string1
expectedInput2=string2
foo=string1
bar=string2
bar=string2

Another way could be to implement your ArgumentMatcher still to avoid overlapping:

when(mockObject.method(eq("expectedInput1"))).thenReturn(string1);
when(mockObject.method(eq("expectedInput2"))).thenReturn(string2);
when(
    mockObject.method(
        argThat(
            new ArgumentMatcher<String>(){
                @Override
                public boolean matches(final Object argument) {
                    return !"expectedInput1".equals(argument) 
                        && !"expectedInput2".equals(argument);
                }
            }
        )
    )
).thenReturn(string1, string2);

Another way could be to implement your Answer with something like this:

when(mockObject.method(anyString())).thenAnswer(
    new Answer<String>() {
        Iterator<String> it = Arrays.asList(string1, string2).iterator();
        String result;
        @Override
        public String answer(final InvocationOnMock invocation) throws Throwable {
            String argument = (String) invocation.getArguments()[0];
            switch (argument) {
                case "expectedInput1" :
                    return string1;
                case "expectedInput2" :
                    return string2;
                default:
                    if (it.hasNext()) {
                        result = it.next();
                    }
                    return result;
            }
        }
    }
);
like image 2
Nicolas Filotto Avatar answered Oct 19 '22 08:10

Nicolas Filotto