Skip to content

@Captor test parameters don't work with primitive type arguments #3229

@KierannCode

Description

@KierannCode

That's the first time I'm actually contributing to any public project.
I apologize if I'm missing important informations about this issue.

Description of the error :

I'm using java 21 and the dependency org.mockito:mockito-junit-jupiter version 5.8.0
The issue occurs when using an ArgumentCaptor as a test method parameter annotated with @Captor, if it is used to capture the argument of a method that takes a primitive type, it throws a NullPointerException trying to unbox a null value to the primitive type.
This only occurs when using @Captor on a parameter. It works fine on a field or as a local ArgumentCaptor.
The @Captor parameters also work as expected with non-primitive types (Well kinda, I'll go back to this later)

Reproduction :

Here's the sample code to reproduce the error, followed by the corresponding stacktrace (I had to order the test because the occurence of the error causes all other tests to fail somehow) :

@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ReproductionTest {
    @Mock
    private Foo foo;

    @Captor
    private ArgumentCaptor<Integer> annotatedFieldCaptor;

    @Test
    @Order(1)
    void this_works_fine() {
        ArgumentCaptor<Integer> localCaptor = ArgumentCaptor.forClass(Integer.class);
        testCaptor(localCaptor);
    }

    @Test
    @Order(2)
    void this_works_too() {
        testCaptor(annotatedFieldCaptor);
    }

    @Test
    @Order(3)
    void this_throws_NullPointerException(@Captor ArgumentCaptor<Integer> annotatedParameterCaptor) {
        testCaptor(annotatedParameterCaptor);
    }

    private void testCaptor(ArgumentCaptor<Integer> captor) {
        doNothing().when(foo).doSomething(captor.capture());
        foo.doSomething(1);
        assertEquals(1, captor.getValue());
    }

    static class Foo {
        void doSomething(int value) {
        }
    }
}

Unsurprisingly, the first two tests pass, but the third fails with the following stacktrace :

java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "org.mockito.ArgumentCaptor.capture()" is null

at org.test.ReproductionTest.testCaptor(ReproductionTest.java:44)
at org.test.ReproductionTest.this_throws_NullPointerException(ReproductionTest.java:40)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

Wait, there's more, and my proposition of solution

I investigated the issue using the debugger and noticed pretty quickly what the problem was. The "capture type" was always set to Object, no matter what the generic type inside the ArgumentCaptor was (even non primitive). It's not the case for local and field captors, which have the expected "capture type". Knowing that, since the capture() method return defaultValue(clazz), it always return null. It still works somehow for non primitive method parameters, but null cannot be passed as a primitive method parameter, hence the NullPointerException about the unboxing. So I investigated a bit more in the CaptorParameterResolver class, and I think I know what's wrong. It uses the CaptorAnnotationProcessor.process method, which uses
new GenericMaster().getGenericType(parameter) to determine the generic type to capture.
However, this leads to the invocation of the method GenericMaster.getGenericType(Parameter parameter).
This method uses Parameter.getType() to look for the type parameter, but that actually returns the raw class of the parameter, so the informations about type parameters are lost. In the method GenericMaster.getGenericType(Field field) used for fields, it uses Field.getGenericType() that actually return the type with type parameters. The equivalent of this method for Parameter is Parameter.getParameterizedType().
I think this is clearly a bug, and the fix seems pretty obvious to me :

In GenericMaster.java, line 30, replace
return getaClass(parameter.getType());
by
return getaClass(parameter.getParameterizedType());

I tested it with a workaround and it works as intended (as a proof of concept only)
Furthermore, it seems like this method (and all the captor parameter resolving circuit) isn't used anywhere else, so there's very little impact to expect from this fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions