Mockito verify showing more input values than method allows

59 views Asked by At

Hello stack community,

I'm having trouble validating input parameters of mocked class method because results are showing that it accepts more input values than method allows. I'll try to explain with code snippet below:

TEST:

class MyClass {


    //This is local class from project
    @Mock
    private ElasticSearchService elasticSearchService;

    private final static String INDEX_NAME = "index-test";
    private Consumer<Message<List<Command<Entity>>>> index;

    @BeforeEach
    void beforeEach() {
        index = new EntityIndexFactory(elasticSearchService, new ObjectMapper()).create();
    }


    @Test
    void happyCase() {
        Event event = new Event();
        event.setId("eventId");
        event.setLastUpdate(1L);
        Command<Entity> command = Command.builder()
                .id(event.getId())
                .type("event")
                .version(event.getLastUpdate())
                .actions(Map.of(INDEX_NAME, Action.UPSERT))
                .source(event)
                .build();
        when(elasticSearchService.indexExists(any())).thenReturn(false);
        index.accept(MessageBuilder.createMessage(List.of(command), new MessageHeaders(null)));
        List<DocWriteRequest<?>> processDocs = new ArrayList<>();
        processDocs.add(FactoryTestUtils.upsertRequest(INDEX_NAME, command));
        verify(elasticSearchService).process(processDocs);
    }
}

CLASS I'M TRYING TO TEST:

@Service
@RequiredArgsConstructor
public class EntityIndexFactory {

    @NonNull
    private ElasticSearchService elasticSearchService;

    @NonNull
    private ObjectMapper objectMapper;

    public <E extends Entity> Consumer<Message<List<Command<E>>>> create() {
        return message -> {
            try {
                List<DocWriteRequest<?>> mappedCommands = message.getPayload().stream()
                        .map(this::mapCommand)
                        .flatMap(List::stream)
                        .toList();
                elasticSearchService.process(mapSecondary(mappedCommands));
            } catch (Exception e) {
                throw new PersistenceException(e);
            }
        };
    }

MOCKED CLASS:

@Service
@RequiredArgsConstructor
public class ElasticSearchService {

    @NonNull
    private RestHighLevelClient esClient;

    public boolean process(List<DocWriteRequest<?>> requests) {
        BulkRequest bulkRequest = new BulkRequest();
        try {
            requests.forEach(bulkRequest::add);
            BulkResponse bulkResponse = esClient.bulk(bulkRequest, RequestOptions.DEFAULT);

            boolean responseSuccess = !bulkResponse.hasFailures();

            if (!responseSuccess) {
                log.info("Failed to execute index operation : {}", bulkResponse.buildFailureMessage());
                handleRetryableStatuses(bulkResponse);
            }

            return responseSuccess;

        } catch (IOException ex) {
            throw new RecoverableException("Exception caused during bulk execution", INFO_CODE, ex);
        }
    }

THE ARGUMENT MISMATCH I GET FROM VERIFY() METHOD IN TEST:

NOTE: The difference it's showing "indexExists() is an another method in ElasticSearchService class which is called during mapSecondary() call in EntityIndexorFactory class.

Picture: Argument mismatch

I would expect that assertion comparison would occur only on process() method input parameter which is just one, List<DocWriteRequest<?>>, however it somehow passes another method into it...

EDIT: Added mapSecondary() method for investigation reference.

private List<DocWriteRequest<?>> mapSecondary(List<DocWriteRequest<?>> esRequests) {
    Map<String, Boolean> secondaryIndexExistence = new HashMap<>();
    List<DocWriteRequest<?>> extendedList = new ArrayList<>(esRequests);
    for (DocWriteRequest<?> request : esRequests) {
        String index = request.index();
        if (!secondaryIndexExistence.containsKey(index)) {
            secondaryIndexExistence.put(index, elasticSearchService.indexExists(index + secondaryIndicesSuffix));
        }
        if (Boolean.TRUE.equals(secondaryIndexExistence.get(index))) {
            if (request instanceof IndexRequest indexRequest) {
                extendedList.add(secIndexUpsertRequest(indexRequest));
            } else extendedList.add(secIndexDeleteRequest((DeleteRequest) request));
        }
    }
    return extendedList;

}
2

There are 2 answers

0
MatijaT On BEST ANSWER

Yes, @Lunivore, you're pretty much right on the spot.

The issue is that objects implementing DocWriteRequest do not overwrite equals(), basically reverting back to reference matching. But they are not the same in this case, cause they’re built separately.

Solution is to validate the lists based on their toString() values.

List<DocWriteRequest<?>> expected = List.of(FactoryTestUtils.upsertRequest(EVENT_INDEX, command));
    verify(elasticSearchService, times(1)).process(argThat(actual -> FactoryTestUtils.collectionStringEquals(actual, expected)));

public static boolean collectionStringEquals(Collection<?> actual, Collection<?> expected) {
    return Objects.equals(
            actual.stream().map(Object::toString).sorted().toList(),
            expected.stream().map(Objects::toString).sorted().toList()
    );
}
0
Lunivore On

I think that screenshot represents "Here's what I was expecting" and "Here's what actually happened", i.e.: it's telling you that it was expecting one call and got two, which is correct.

It doesn't expect indexExists() because that's just a stub, and Mockito does "nice mocking" where you don't have to explicitly say what you expect. It just responds with what you set up. That's why you only see one expectation on the left; because you only set up one verify. But both those calls happened, hence two on the right.

Inspect the other difference carefully. You will probably find that the inequality is in the lists; that FactoryTestUtils.upsertRequest(INDEX_NAME, command) doesn't actually produce something that is equal to the result of mapSecondary(...).

Mockito uses the equals() method of objects to compare them, so if your DocWriteRequest doesn't have that implemented, your two lists are not equal and the arguments won't match.

There are some ways around that; this question's answers, particularly the ArgumentCaptor, might help you.