0

I encountered to a very strange behavior in case I change the mock behavior during the test. I mock a very simple interface:

interface Bar { 
    String string(String str); 
}

@Mock
private Bar bar;

Then I call it and count the number of invocations using AtomicInteger, which serves as a side-effect for this minimal working example.

@Test
public void test() {

    AtomicInteger atomicInteger = new AtomicInteger(0);

    // Mock with the increment
    Mockito.when(bar.string(Mockito.anyString())).then(invocation -> {    
        log.info("MOCK - waiting (1): {}", invocation.getArguments()[0]);
        atomicInteger.incrementAndGet();
        log.info("MOCK - returning (1)");
        return "BAR_1";
    });
    // Invocation of the increment
    log.info("Result (1): " + bar.string("FOO_1"));                       

    // Passes
    Assertions.assertEquals(1, atomicInteger.get());                      
}
14:18:17.336 [main] INFO com.Foo - MOCK - waiting (1): FOO_1
14:18:17.343 [main] INFO com.Foo - MOCK - returning (1)
14:18:17.349 [main] INFO com.Foo - Result (1): BAR_1

The test passes as long as the method was obviously invoked once using bar.string("FOO_1"). As long as I add the new behavior of the mock bar after this executes to not increment the AtomicInteger, there happens one more invocation of the original mock that should not be called:

@Test
public void test() {

    AtomicInteger atomicInteger = new AtomicInteger(0);

    // Mock with the increment
    Mockito.when(bar.string(Mockito.anyString())).then(invocation -> {
        log.info("MOCK - waiting (1): {}", invocation.getArguments()[0]);
        atomicInteger.incrementAndGet();
        log.info("MOCK - returning (1)");
        return "BAR_1";
    });

    // Invocation with increment
    log.info("Result (1): " + bar.string("FOO_1"));

    /* NEW CODE BLOCK STARTS */

    // Mock without the increment
    Mockito.when(bar.string(Mockito.anyString())).then(invocation -> {    
         log.info("MOCK - returning (2): {}", invocation.getArguments()[0]);
         return "BAR_2";
    });

    // Invocation without the increment
    // The previous lines really changed the mock, but it was called one more times
    log.info("Result (2): " + bar.string("FOO_2"));

    /* NEW CODE BLOCK ENDS */

    // Fails, it is 2
    Assertions.assertEquals(1, atomicInteger.get());                      
}
14:19:31.603 [main] INFO com.Foo - MOCK - waiting (1): FOO_1
14:19:31.612 [main] INFO com.Foo - MOCK - returning (1)
14:19:31.620 [main] INFO com.Foo - Result (1): BAR_1
14:19:31.621 [main] INFO com.Foo - MOCK - waiting (1): 
14:19:31.621 [main] INFO com.Foo - MOCK - returning (1)
14:19:31.623 [main] INFO com.Foo - MOCK - returning (2): FOO_2
14:19:31.624 [main] INFO com.Foo - Result (2): BAR_2

Surprisingly, the log shows the mocked method was called with no parameter on the 4th line.

The behavior doesn't change when I include more of this block of code within the same test N-times. The test always fails expecting the increment to be 2 instead of 1.

Mockito.when(bar.string(Mockito.anyString())).then(invocation -> {
     log.info("MOCK - returning (N): {}", invocation.getArguments()[0]);
     return "BAR_N";
});
log.info("Result (N): " + bar.string("FOO_N"));                       

What makes Mockito call exactly one time the mocked method with mocked parameters after its behavior is changed during the test 1+ times?

Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183
  • You're not actually manually writing the call count and argument checks are you? There are ready made mechanisms for them. – Kayaman Jul 22 '20 at 12:36
  • @Kayaman: It's an irrelevant point. I demonstrate the odd behvaior of Mockito when it comes to side-effects with a minimal working example. Consider `AtomicInteger::incrementAndGet` as a side-effect method. – Nikolas Charalambidis Jul 22 '20 at 12:40
  • You're *remocking* a method to do something unnecessary. I needed to verify that you're aware that you're doing something weird, so you should also not be surprised that you're getting weird results. The [correct way](https://stackoverflow.com/questions/4216569/how-to-tell-a-mockito-mock-object-to-return-something-different-the-next-time-it) would be to reset or rebuild your mock, the reason for this result is in the internal implementation. – Kayaman Jul 22 '20 at 12:43
  • I am remocking a method to not do the side effect (thus no increment) and to demonstrate the original mocked method (with increment) is somehow called twice although it should be called only once when being invoked once. The second invocation happens on the remocked method with no side-effect (no increment). Actually, the original method is *still* somehow called one more time. I am aware what I do, that's why I am surprised of the result it gives me. How would you explain the 4th line in the 2nd log? – Nikolas Charalambidis Jul 22 '20 at 12:47
  • Why would you think `bar.string("FOO_1")` increases the `AtomicInteger` and `bar.string(Mockito.anyString())` when you are trying to remock the method doesn't? They both call the mocked method that increases the number, which is why you shouldn't be trying to remock the method! It's a method call, the only place where it definitely won't have side effects is with an *empty* mock, a previously mocked method is **not** empty. – Kayaman Jul 22 '20 at 12:52
  • That's the point! I didn't expect `Mockito.when(bar.string(Mockito.anyString()))....` construct would call the actual mock itself. But it makes sense it does. Compose the answer, please. – Nikolas Charalambidis Jul 22 '20 at 12:56

1 Answers1

1

Calling bar.string(Mockito.anyString()) in the remocking operation is still equivalent to a bar.string(String) call, which was mocked earlier to increase the AtomicInteger.

log.info("Result (1): " + bar.string("FOO_1"));  // Increases to 1

Mockito.when(bar.string(Mockito.anyString())).then(invocation -> {  // Increases to 2

After the remocking, the number is no longer incremented as the new mock has taken effect. You should work with clean mocks to avoid writing brittle or complex test code.

Kayaman
  • 72,141
  • 5
  • 83
  • 121