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.
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:
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.
@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.
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.