Using ArgumentCaptor to capture a list of specific type with Mockito

May 07, 2022 No comments Mockito Java JUnit Test

1. Introduction

In this article, we will learn how to capture a list of a specific type with Mockito. We will present two approaches to creating an ArgumentCaptor object.

2. Test class

Let's start with our test class:

package com.frontbackend.libraries.mockito.service;

import java.util.Arrays;
import java.util.List;

public class ProcessingService {

    private final ListProcessor listProcessing;

    public ProcessingService(ListProcessor listProcessing) {
        this.listProcessing = listProcessing;
    }

    public List<String> processList(String str) {
        List<String> list = Arrays.asList(str, str, str);
        return this.listProcessing.processList(list);
    }
}

The ProcessingService is a simple service class that uses injected ListProcessor for processing a list of strings.

package com.frontbackend.libraries.mockito.service;

import java.util.List;
import java.util.stream.Collectors;

public class ListProcessor {

    public List<String> processList(List<String> list) {
        return list.stream()
                .map(str -> String.format("%s:processed", str))
                .collect(Collectors.toList());
    }
}

The ListProcessor service will iterate over each item in the list and add processed text at the end.

3. Using @Captor annotation

We can avoid all nested generics-problem with the @Captor annotation:

package com.frontbackend.libraries.mockito;

import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasProperty;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.frontbackend.libraries.mockito.service.ListProcessor;
import com.frontbackend.libraries.mockito.service.ProcessingService;
import java.util.Arrays;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class MockitoProcessingServiceTest {

    @Mock
    private ListProcessor listProcessor;

    @InjectMocks
    private ProcessingService processingService;

    @Captor
    private ArgumentCaptor<List<String>> captor;

    @Test
    public void shouldProcessList() {
        when(listProcessor.processList(anyList())).thenCallRealMethod();

        List<String> result = processingService.processList("test");
        verify(listProcessor).processList(captor.capture());

        List<String> captured = captor.getValue();
        Assert.assertEquals(3, captured.size());

        assertThat(captured, is(Arrays.asList("test", "test", "test")));
        assertThat(result, is(Arrays.asList("test:processed", "test:processed", "test:processed")));
    }
}

In this example JUnit test first we:

  • configure the ListProcessor to call a real method whenever we use processList(...),
  • next, we call ProcessingService.processList(...) method with test String as an argument,
  • in the next line: verify(listProcessor).processList(captor.capture()); - Mockito.verify(...) checks if this specific method was called. Additionally we added captor.capture() to check with what parameter this method was called,
  • captor.getValue() returns the object that was used as an argument to listProcessor.processList(...),
  • finally, we did some asserts to check if logic works as expected.

Note that we used hamcrest library to check if lists contains expected items:

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>

4. Creating ArgumentCaptor object in the method body

We could also create an ArgumentCaptor inside the testing method:

package com.frontbackend.libraries.mockito;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.frontbackend.libraries.mockito.service.ListProcessor;
import com.frontbackend.libraries.mockito.service.ProcessingService;
import java.util.Arrays;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class MockitoProcessingServiceTest {

    @Mock
    private ListProcessor listProcessor;

    @InjectMocks
    private ProcessingService processingService;

    @Test
    public void shouldProcessList() {
        when(listProcessor.processList(anyList())).thenCallRealMethod();

        @SuppressWarnings("unchecked")
        ArgumentCaptor<List<String>> listCaptor = ArgumentCaptor.forClass(List.class);

        List<String> result = processingService.processList("test");
        verify(listProcessor).processList(listCaptor.capture());

        List<String> captured = listCaptor.getValue();
        Assert.assertEquals(3, captured.size());

        assertThat(captured, is(Arrays.asList("test", "test", "test")));
        assertThat(result, is(Arrays.asList("test:processed", "test:processed", "test:processed")));
    }
}

This approach uses kind of java old-style semantics. IDE will warn you about the use of unchecked or unsafe operations. That's why we added @SuppressWarnings("unchecked") - to hide such warning.

5. Conclusion

In this article, we showed how to capture a list of a specific type using Mockito. We of course prefer a solution with @Captor annotation - it is clean and doesn't report any warning.

As usual, code introduced in this article is available in our GitHub repository.

{{ message }}

{{ 'Comments are closed.' | trans }}