Search code examples
unit-testingkotlinmockitopageable

Mockito - Kotlin test throws Null Pointer Exception when trying to capture Pageable argument


I have written a really simple test for a method in my controller using Mockito

@Test
fun `get items based on category ID`() {
    val pageable: Pageable = PageRequest.of(5, 50)
    controller.get(10, pageable)

    val captor = ArgumentCaptor.forClass(Int::class.java)
    val pageableCaptor = ArgumentCaptor.forClass(Pageable::class.java)
    Mockito.verify(itemService).getItemsBasedOnCategoryID(captor.capture(), pageableCaptor.capture())
    assertEquals(captor.value, 10)
    assertEquals(pageableCaptor.value.pageSize, 50)
    assertEquals(pageableCaptor.value.pageNumber, 5)
}

But I get this exception

pageableCaptor.capture() must not be null
java.lang.NullPointerException: pageableCaptor.capture() must not be null
    at com.practice.ItemControllerTest.get items based on category ID(ItemControllerTest.kt:41)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

which I am not able to understand because when I test the method directly on the service layer using a similar code it passes the test. I have a workaround for this test but I am just trying to understand why this is not working. I would really appreciate some help with this.

If there is any other information that you would like me to add please feel free to let me know.


Solution

  • The problem is that the pageable argument of the getItemsBasedOnCategoryID is non-nullable, while the return type of the ArgumentCaptor.capture is a platform type, which is considered by the Kotlin compiler as possibly nullable (and actually capture() returns null, that's how Mockito works). In such a case, the compiler generates null checks, when the type is used. You can see it in the decompiled code of your test:

    @Test
    public final void get_items_based_on_category_ID {
          ...
          Object var10002 = pageableCaptor.capture();
          Intrinsics.checkNotNullExpressionValue(var10002, "pageableCaptor.capture()"); <<- NPE
          var10000.getItemsBasedOnCategoryID(var4, (Pageable)var10002);
          ...
    }
    

    The trick is to somehow fool the compiler to prevent it from generating null-checks.

    Option 1: Use mockito-kotlin library. It provides work-around for these kind of issues plus several additional tools. It might be your best choice, as probably you are going to face next issues, e.g. when using Mockito's any() argument matcher (same story, null- vs non-null mismatch)

    Option 2: DIY:

    1. Firstly, explicitly declare ArgumentCapture's type parameter as non-nullable:
    val pageableCaptor: ArgumentCaptor<Pageable> = ArgumentCaptor.forClass(Pageable::class.java)
    

    Without the explicit declaration, the type of the pageableCaptor is ArgumentCaptor<Pageable!>!, i.e. a platform type.

    1. Then you are going to need a helper function:
    @Suppress("UNCHECKED_CAST")
    private fun <T> capture(captor: ArgumentCaptor<T>): T = captor.capture()
    

    It seems to be a no-op function, but the point is that it does not return a platform type anymore: if the ArgumentCaptor's type parameter in non-nullable, so is the type of the function return value.

    1. And finally use this function instead of the ArgumentCaptor.capture():
    Mockito.verify(itemService).getItemsBasedOnCategoryID(captor.capture(), capture(pageableCaptor))
    

    Now the Kotlin compiler believes that capture(pageableCaptor) never returns null, so it does not generate any null-checks.