Search code examples
javaspring-data-jpamockito

Mocking object called in lambda invoked from mocked object


I am handling transactions in Spring boot 2.7 manually:

dataHeader = transactionTemplate.execute(status -> {
    final var doc = findDoc(document);
    return saveReport(doc);
});

private Doc findDoc(DocumentDto document) {
    return docRepository.findByDocType(document.getType(), document.getId()), document.getReport()).get(0);
}

I found this answer hot to test such case, so I wrote this:

when(docRepository.findByDocType(any(), any(), any())).thenReturn(List.of());
when(transactionTemplate.execute(any(TransactionCallback.class)))
        .then(invocation -> ((TransactionCallback<Doc>) invocation.getArgument(0))
                .doInTransaction(any(TransactionStatus.class)));

var log = LoggerWithMemoryFactory.getBufferedLoggerApi(DocService.class);
final var actual = logEvent.getMessage().getStringPayload();
if (!actual.contains(expected)) {
    var message = String.format("expected: '%s' in '%s'", expected, actual);
    throw new AssertionFailedError(message, expected, actual);
}

AFAIK I am not mixing any() matchers with values (like "X" or 10L). But still my test fails on wrong number of matchers for docRepository.findByDocType.

org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers! 3 matchers expected, 1 recorded

For curiosity, this works well in a debugger when I put a breakpoint before docRepository.findByDocType.

I found this Mockito ticket, which looks relevant but I did not understand it and I don't know how to apply it to my situation.

Update:

When I taken the code using mocked object out of lambda, then the test works well:

final var doc = docRepository.findByDocType(document.getType(), document.getId()), document.getReport()).get(0);
dataHeader = transactionTemplate.execute(status -> {
    return saveReport(doc);
});

Solution:

Finally, it works when written this way:

doAnswer(invocation -> {
    TransactionCallback<List<Doc>> callback = invocation.getArgument(0);
    return callback.doInTransaction(new SimpleTransactionStatus());
}).when(transactionTemplate).execute(any());

Solution

  • AFAIK I am not mixing any() matchers with values (like "X" or 10L)

    No, but you are pushing Matchers onto the Mockito stack outside of a when call:

    when(transactionTemplate.execute(any(TransactionCallback.class)))
            .then(invocation -> invocation.<TransactionCallback<Doc>>getArgument(0)
                    .doInTransaction(any(TransactionStatus.class)));
                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^ you must not use a matcher when calling a method
    

    What do you expect this code to do? .doInTransaction(any(…)) doesn't make any sense (and any(…) simply does a return null after the matcher was recorded). Try it out (in a separate test):

    assertNull(any(TransactionStatus.class));
    

    You must call the method with a real argument (but if you don't use status, then null might work as well).

    Also, transactionTemplate MUST be a Mockito mock object (@Mock, Mockito.mock(), @Spy, or Mockito.spy()). If it's not a mock, you can not record matchers for it and can not define stubs for its method calls.