How to capture a list of specific type with mockito
Asked Answered
S

9

400

Is there a way to capture a list of specific type using mockitos ArgumentCaptore. This doesn't work:

ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(ArrayList.class);
Switcheroo answered 9/4, 2011 at 17:17 Comment(1)
I find that it's a terrible idea to use concrete list implementation here (ArrayList). You can always use List interface, and if you want represent the fact, that it's covariant, then you can use extends: ArgumentCaptor<? extends List<SomeType>>Triiodomethane
E
676

The nested generics-problem can be avoided with the @Captor annotation:

public class Test{

    @Mock
    private Service service;

    @Captor
    private ArgumentCaptor<ArrayList<SomeType>> captor;

    @Before
    public void init(){
        MockitoAnnotations.initMocks(this);
    }

    @Test 
    public void shouldDoStuffWithListValues() {
        //...
        verify(service).doStuff(captor.capture()));
    }
}
Earleanearleen answered 13/4, 2011 at 21:12 Comment(8)
I prefer using MockitoAnnotations.initMocks(this) in the @Before method rather than using a runner that excludes the ability to use another runner. However, +1, thanks for pointing out the annotation.Meredeth
Not sure this example is complete. I get... Error:(240, 40) java: variable captor might not have been initialized i like tenshi's answer belowMoravia
I ran into the same issue, and found this blog post which helped me a bit: blog.jdriven.com/2012/10/…. It includes a step to use MockitoAnnotations.initMocks after you've put the annotation on your class. One thing I noticed is you can't have it within a local variable.Protectionist
@chamzz.dot ArgumentCaptor<ArrayList<SomeType>> captor; is already capturing an array of "SomeType" <-- that is a specific type, isn't?Deontology
I usually prefer List instead of ArrayList in the Captor declaration: ArgumentCaptor<List<SomeType>> captor;Deontology
I had to use org.junit.jupiter.api.BeforeEach instead of Before in my tests.Caliginous
initMocks(this) should be replaced with openMocks(this), as the first one ist deprecatedPiddle
This worked for me with but in my implementation I use @org.junit.jupiter.api.extension.ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) annotation at class level instead of MockitoAnnotations.initMocks(this);.Goodness
X
178

Yeah, this is a general generics problem, not mockito-specific.

There is no class object for ArrayList<SomeType>, and thus you can't type-safely pass such an object to a method requiring a Class<ArrayList<SomeType>>.

You can cast the object to the right type:

Class<ArrayList<SomeType>> listClass =
              (Class<ArrayList<SomeType>>)(Class)ArrayList.class;
ArgumentCaptor<ArrayList<SomeType>> argument = ArgumentCaptor.forClass(listClass);

This will give some warnings about unsafe casts, and of course your ArgumentCaptor can't really differentiate between ArrayList<SomeType> and ArrayList<AnotherType> without maybe inspecting the elements.

(As mentioned in the other answer, while this is a general generics problem, there is a Mockito-specific solution for the type-safety problem with the @Captor annotation. It still can't distinguish between an ArrayList<SomeType> and an ArrayList<OtherType>.)

Edit:

Take also a look at tenshi's comment. You can change the original code to this simplified version:

final ArgumentCaptor<List<SomeType>> listCaptor
        = ArgumentCaptor.forClass((Class) List.class);
Xylophagous answered 9/4, 2011 at 18:3 Comment(4)
The example you showed can be simplified, based on the fact that java makes type inference for the static method calls: ArgumentCaptor<List<SimeType>> argument = ArgumentCaptor.forClass((Class) List.class);Triiodomethane
To disable the uses unchecked or unsafe operations warning, use the @SuppressWarnings("unchecked") annotation above the argument captor definition line. Also, casting to Class is redundant.Sesquipedalian
The casting to Class is not redundant in my tests.Mating
It's not even necessary to use a captor to get at a collection passed to a method, if the collection was created with List.of(), Set.of() (which can't be modified)Steeplechase
G
24

If you're not afraid of old java-style (non type safe generic) semantics, this also works and is simple'ish:

ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
verify(subject).method(argument.capture()); // run your code
List<SomeType> list = argument.getValue(); // first captured List, etc.
Glasser answered 1/12, 2014 at 21:44 Comment(1)
You can add @SuppressWarnings("rawtypes") before the declaration to disable warnings.Stott
F
9
List<String> mockedList = mock(List.class);

List<String> l = new ArrayList();
l.add("someElement");

mockedList.addAll(l);

ArgumentCaptor<List> argumentCaptor = ArgumentCaptor.forClass(List.class);

verify(mockedList).addAll(argumentCaptor.capture());

List<String> capturedArgument = argumentCaptor.<List<String>>getValue();

assertThat(capturedArgument, hasItem("someElement"));
Fiddler answered 19/4, 2016 at 8:56 Comment(0)
S
8

Based on @tenshi's and @pkalinow's comments (also kudos to @rogerdpack), the following is a simple solution for creating a list argument captor that also disables the "uses unchecked or unsafe operations" warning:

@SuppressWarnings("unchecked")
final ArgumentCaptor<List<SomeType>> someTypeListArgumentCaptor =
    ArgumentCaptor.forClass(List.class);

Full example here and corresponding passing CI build and test run here.

Our team has been using this for some time in our unit tests and this looks like the most straightforward solution for us.

Sesquipedalian answered 30/5, 2018 at 17:17 Comment(0)
A
3

For an earlier version of junit, you can do

Class<Map<String, String>> mapClass = (Class) Map.class;
ArgumentCaptor<Map<String, String>> mapCaptor = ArgumentCaptor.forClass(mapClass);
Aleenaleetha answered 16/1, 2019 at 0:8 Comment(1)
This just results in a different Unchecked assignment warning.Lingerfelt
S
1

I had the same issue with testing activity in my Android app. I used ActivityInstrumentationTestCase2 and MockitoAnnotations.initMocks(this); didn't work. I solved this issue with another class with respectively field. For example:

class CaptorHolder {

        @Captor
        ArgumentCaptor<Callback<AuthResponse>> captor;

        public CaptorHolder() {
            MockitoAnnotations.initMocks(this);
        }
    }

Then, in activity test method:

HubstaffService hubstaffService = mock(HubstaffService.class);
fragment.setHubstaffService(hubstaffService);

CaptorHolder captorHolder = new CaptorHolder();
ArgumentCaptor<Callback<AuthResponse>> captor = captorHolder.captor;

onView(withId(R.id.signInBtn))
        .perform(click());

verify(hubstaffService).authorize(anyString(), anyString(), captor.capture());
Callback<AuthResponse> callback = captor.getValue();
Synchro answered 24/8, 2015 at 9:48 Comment(0)
P
1

There is an open issue in Mockito's GitHub about this exact problem.

I have found a simple workaround that does not force you to use annotations in your tests:

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.MockitoAnnotations;

public final class MockitoCaptorExtensions {

    public static <T> ArgumentCaptor<T> captorFor(final CaptorTypeReference<T> argumentTypeReference) {
        return new CaptorContainer<T>().captor;
    }

    public static <T> ArgumentCaptor<T> captorFor(final Class<T> argumentClass) {
        return ArgumentCaptor.forClass(argumentClass);
    }

    public interface CaptorTypeReference<T> {

        static <T> CaptorTypeReference<T> genericType() {
            return new CaptorTypeReference<T>() {
            };
        }

        default T nullOfGenericType() {
            return null;
        }

    }

    private static final class CaptorContainer<T> {

        @Captor
        private ArgumentCaptor<T> captor;

        private CaptorContainer() {
            MockitoAnnotations.initMocks(this);
        }

    }

}

What happens here is that we create a new class with the @Captor annotation and inject the captor into it. Then we just extract the captor and return it from our static method.

In your test you can use it like so:

ArgumentCaptor<Supplier<Set<List<Object>>>> fancyCaptor = captorFor(genericType());

Or with syntax that resembles Jackson's TypeReference:

ArgumentCaptor<Supplier<Set<List<Object>>>> fancyCaptor = captorFor(
    new CaptorTypeReference<Supplier<Set<List<Object>>>>() {
    }
);

It works, because Mockito doesn't actually need any type information (unlike serializers, for example).

Penzance answered 12/6, 2020 at 8:42 Comment(0)
U
0

Since Mockito 5.3.0 you can circumvent that problem with the assertArg matcher.1

Assuming you want to do assertions on a method with signature

interface MyInterface {
    MyResultType calculateSomething(List<MyType> aList)
}

you can just call (using AssertJ here)

verify(myInterfaceMock).calculateSomething(
  assertArg(theList -> assertThat(theList)
    //inside here theList has the correct type
    .isNotNull()
    .isNotEmpty()
  )
);

For readability i prefer to put the assertions into a variable.

final Consumer<? extends List<MyType>> myListAssertions = theList -> 
  assertThat(theList)
    .isNotNull()
    .isNotEmpty();

verify(myInterfaceMock).calculateSomething( assertArg(myListAssertions) );

This works well, with failed assertions pointing to the correct line where the actual assertion is defined.

Unschooled answered 8/1 at 15:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.