javamockitoverify

Mockito verify showing more input values than method allows


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;

}

Solution

  • 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()
        );
    }