Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
KierannCode opened this issue Jan 8, 2024 · 1 comment · Fixed by #3257
Closed

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

KierannCode opened this issue Jan 8, 2024 · 1 comment · Fixed by #3257

Comments

@KierannCode
Copy link

KierannCode commented Jan 8, 2024

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.

@KierannCode KierannCode changed the title @Captor test parameters don't work with primitive type arguments @Captor test parameters don't work with primitive type arguments Jan 8, 2024
@youssef3wi
Copy link
Contributor

You can use the @ParameterizedTest annotation as a workaround.

youssef3wi added a commit to youssef3wi/mockito that referenced this issue Feb 1, 2024
youssef3wi added a commit to youssef3wi/mockito that referenced this issue Feb 2, 2024
youssef3wi added a commit to youssef3wi/mockito that referenced this issue Feb 2, 2024
youssef3wi added a commit to youssef3wi/mockito that referenced this issue Feb 2, 2024
TimvdLippe pushed a commit that referenced this issue Feb 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants