63

I have a problem with capturing the Class argument via ArgumentCaptor. My test class looks like this:

@RunWith(RobolectricGradleTestRunner::class)
@Config(sdk = intArrayOf(21), constants = BuildConfig::class)
class MyViewModelTest {
    @Mock
    lateinit var activityHandlerMock: IActivityHandler;

    @Captor
    lateinit var classCaptor: ArgumentCaptor<Class<BaseActivity>>

    @Captor
    lateinit var booleanCaptor: ArgumentCaptor<Boolean>

    private var objectUnderTest: MyViewModel? = null

    @Before
    fun setUp() {
        initMocks(this)
        ...
        objectUnderTest = MyViewModel(...)
    }

    @Test
    fun thatNavigatesToAddListScreenOnAddClicked(){
        //given

        //when
        objectUnderTest?.addNewList()

        //then
        verify(activityHandlerMock).navigateTo(classCaptor.capture(), booleanCaptor.capture())
        var clazz = classCaptor.value
        assertNotNull(clazz);
        assertFalse(booleanCaptor.value);
    }
}

When I run the test, following exception is thrown:
java.lang.IllegalStateException: classCaptor.capture() must not be null
Is it possible to use argument captors in kotlin?

========= UPDATE 1:
Kotlin: 1.0.0-beta-4584
Mockito: 1.10.19
Robolectric: 3.0

========= UPDATE 2:
Stacktrace:

java.lang.IllegalStateException: classCaptor.capture() must not be null

at com.example.view.model.ShoplistsViewModelTest.thatNavigatesToAddListScreenOnAddClicked(ShoplistsViewModelTest.kt:92)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.robolectric.RobolectricTestRunner$2.evaluate(RobolectricTestRunner.java:251)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:188)
at org.robolectric.RobolectricTestRunner.runChild(RobolectricTestRunner.java:54)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.robolectric.RobolectricTestRunner$1.evaluate(RobolectricTestRunner.java:152)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:234)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:74)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Ramps
  • 5,190
  • 1
  • 45
  • 47
  • Just tried [your example](https://gist.github.com/miensol/d2a3c070f9efc50d8d2f) and it works just fine. What are versions of kotlin, mockito and roboelectric that you're using? – miensol Jan 13 '16 at 22:10
  • Thanks for checking. I have updated the question. Which versions did you try? – Ramps Jan 14 '16 at 16:28
  • Just rechecked [the sample from gist](https://gist.github.com/miensol/d2a3c070f9efc50d8d2f) and it works fine with the versions you've listed. Seems that your code must be somewhat different. Can you post a stacktrace? – miensol Jan 14 '16 at 20:17
  • I've added the stacktrace. Unfortunately it doesn't seem very helpful. – Ramps Jan 15 '16 at 18:44
  • You're right it does not. It seems strange though that you have `RobolectricTestRunner` in stack trace even though the `RunWith` says `RobolectricGradleTestRunner` – miensol Jan 15 '16 at 20:35
  • Could the problem be that the return value of classCaptor.capture() is null? Kotlin then thinks that the signature of IActivityHandler#navigateTo(Class, Boolean) cannot allow a null argument? – Mike Buhot Jan 16 '16 at 11:24
  • 2
    Yes, that's it. You are right. I didn't notice that the navigateTo method accepts only non-null values. When I changed singature of navigateTo method to this one: fun navigateTo(clazz: Class?, closeCurrent: Boolean), then example works fine. If you want, please, add an answer to this question, so in case anyone else has the same problem he can find the solution. – Ramps Jan 16 '16 at 13:22
  • @MBuhot See Ramps's comment about posting an answer. – heenenee Jan 29 '16 at 06:14

9 Answers9

68

From this blog

"Getting matchers to work with Kotlin can be a problem. If you have a method written in kotlin that does not take a nullable parameter then we cannot match with it using Mockito.any(). This is because it can return void and this is not assignable to a non-nullable parameter. If the method being matched is written in Java then I think that it will work as all Java objects are implicitly nullable."

A wrapper function is needed that returns ArgumentCaptor.capture() as nullable type.

Add the following as a helper method to your test

fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()

Please see, MockitoKotlinHelpers.kt provided by Google in the Android Architecture repo for reference. the capture function provides a convenient way to call ArgumentCaptor.capture(). Call

verify(activityHandlerMock).navigateTo(capture(classCaptor), capture(booleanCaptor))

Update: If the above solution does not work for you, please check Roberto Leinardi's solution in the comments below.

CanonicalBear
  • 367
  • 8
  • 12
Ikechukwu Kalu
  • 1,474
  • 14
  • 15
  • 1
    Thanks, it works fo me. The part of GoogleSample about UnitTest is well worth leanrning! – angryd Dec 26 '17 at 06:00
  • 1
    I'm glad it works for you, angryd. Sure!, I basically refer to their sample whenever I get stuck and it's also well documented. – Ikechukwu Kalu Dec 28 '17 at 01:39
  • 1
    I think this should be the accepted answer. Using fun capture(captor: ArgumentCaptor) helped me without having to add mockito-kotlin: – Kaskasi Jan 09 '18 at 06:29
  • Sure, @Kaskasi. It's way simpler to use. Thanks. – Ikechukwu Kalu Jan 09 '18 at 11:54
  • 4
    I am still having the same issue after adding: `fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture()` Then in the test: `val userCacheCaptor = ArgumentCaptor.forClass(User::class.java) Mockito.verify(userCache, Mockito.times(1)).set(Mockito.anyString(), capture(userCacheCaptor))` the error is: `java.lang.IllegalStateException: capture(userCacheCaptor) must not be null ` – vlio20 Aug 18 '18 at 19:29
  • @vlio20, how about casting the userCacheCaptor to the User class? More like: `Mockito.verify(eq(userCache), Mockito.times(1)).set(Mockito.anyString(), capture(userCacheCaptor) as User)`. Sorry for the late reply – Ikechukwu Kalu Aug 22 '18 at 04:31
  • 11
    @IkechukwuKalu I had the same issue than I solved using the annotation `@Captor private lateinit var snackbarManagerArgumentCaptor: ArgumentCaptor` and then the helper `verify(snackbarManager).show(capture(snackbarManagerArgumentCaptor))`. – Roberto Leinardi Aug 01 '19 at 06:33
  • The solution @RobertoLeinardi suggested worked for me. – funct7 Aug 17 '20 at 09:19
  • Works for me. This should be the accepted answer. – CanonicalBear Feb 07 '21 at 15:42
  • This was so helpful, thanks a lot – lbal Jun 09 '23 at 18:32
46

The return value of classCaptor.capture() is null, but the signature of IActivityHandler#navigateTo(Class, Boolean) does not allow a null argument.

The mockito-kotlin library provides supporting functions to solve this problem.

Code should be:

    @Captor
    lateinit var classCaptor: ArgumentCaptor<Class<BaseActivity>>

    @Captor
    lateinit var booleanCaptor: ArgumentCaptor<Boolean>

    ...

    @Test
    fun thatNavigatesToAddListScreenOnAddClicked(){
        //given

        //when
        objectUnderTest?.addNewList()

        //then
        verify(activityHandlerMock).navigateTo(
com.nhaarman.mockitokotlin2.capture<Class<BaseActivity>>(classCaptor.capture()), 
com.nhaarman.mockitokotlin2.capture<Boolean>(booleanCaptor.capture())
)
        var clazzValue = classCaptor.value
        assertNotNull(clazzValue);
        val booleanValue = booleanCaptor.value
        assertFalse(booleanValue);
    }

OR

var classCaptor = com.nhaarman.mockitokotlin2.argumentCaptor<Class<BaseActivity>>()
var booleanCaptor = com.nhaarman.mockitokotlin2.argumentCaptor<Boolean>()
...
 verify(activityHandlerMock).navigateTo(
classCaptor.capture(), 
booleanCaptor.capture()
)

also in build.gradle add this:

testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
NickUnuchek
  • 11,794
  • 12
  • 98
  • 138
Mike Buhot
  • 4,790
  • 20
  • 31
  • 44
    Just adding to the above answer for anyone else searching for this: What you need is com.nhaarman.mockito_kotlin.capture(classCaptor) instead of classCaptor.capture() – BhathiyaW Nov 08 '16 at 00:59
  • 3
    And [here](https://gist.github.com/spookyUnknownUser/f0ab1d3b9a99a87c0c0e0155abac81c9) is how a Test from google codelabs would look with the argument Captor – nmu Apr 17 '17 at 08:03
  • 4
    I tried many times to apply com.nhaarman.mockito_kotlin.capture but couldn't success. See https://stackoverflow.com/questions/38715702/mockito-argumentcaptor-for-kotlin-function. There is a link to a wiki: https://github.com/nhaarman/mockito-kotlin/wiki/Mocking-and-verifying#argument-captors. So, I tried to remove several Mockito* libraries from import, change `verify` to com.nhaarman.mockito_kotlin.verify, a problem disappeared. – CoolMind May 23 '18 at 14:13
  • 3
    I had to explicitly specify that the T type should not be null : `com.nhaarman.mockitokotlin2.capture(argumentCaptor)` – Achraf Amil Jul 27 '19 at 14:19
  • @CoolMind that was helpful. Thanks. For everyone else, please [see my answer](https://stackoverflow.com/a/57268842/1276636) which states what to do explicitly. – Sufian Jul 30 '19 at 09:50
  • Thanks for this... phew! Despite all the features in Kotlin I started to dislike it... it's so unreadable and the codes are basically like encrypted – xbmono Jul 07 '22 at 00:37
20

Use kotlin-mockito https://mvnrepository.com/artifact/com.nhaarman/mockito-kotlin/1.5.0 as dependency and sample code as written below :

argumentCaptor<Hotel>().apply {
    verify(hotelSaveService).save(capture())

    assertThat(allValues.size).isEqualTo(1)
    assertThat(firstValue.name).isEqualTo("İstanbul Hotel")
    assertThat(firstValue.totalRoomCount).isEqualTo(10000L)
    assertThat(firstValue.freeRoomCount).isEqualTo(5000L)
}
user3738870
  • 1,415
  • 2
  • 12
  • 24
enes.acikoglu
  • 455
  • 5
  • 14
7

As stated by CoolMind in the comment, you first need to add gradle import for Kotlin-Mockito and then shift all your imports to use this library. Your imports will now look like:

import com.nhaarman.mockitokotlin2.argumentCaptor
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.isNull
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify

Then your test class will be something like this:

val mArgumentCaptor = argumentCaptor<SignUpInteractor.Callback>()

@Test
fun signUp_success() {
    val customer = Customer().apply {
        name = "Test Name"
        email = "test@example.com"
        phone = "0123444456789"
        phoneDdi = "+92"
        phoneNumber = ""
        countryCode = "92"
        password = "123456"
    }
    mPresenter.signUp(customer)
    verify(mView).showProgress()
    verify(mInteractor).createAccount(any(), isNull(), mArgumentCaptor.capture())
}
Sufian
  • 6,405
  • 16
  • 66
  • 120
6

According this solution my solution here:

fun <T> uninitialized(): T = null as T

//open verificator
val verificator = verify(activityHandlerMock)

//capture (would be same with all matchers)
classCaptor.capture()
booleanCaptor.capture()

//hack
verificator.navigateTo(uninitialized(), uninitialized())
Community
  • 1
  • 1
neworld
  • 7,757
  • 3
  • 39
  • 61
  • I have no idea why, but this is the only solution that has worked for me so far. Although technically, the others should work as well, as all they seem to do is a generic cast of null to T. – bompf May 20 '19 at 13:09
0

Came here after the kotlin-Mockito library didn't help. I created a solution using reflection. It is a function which extracts the argument provided to the mocked-object earlier:

fun <T: Any, S> getTheArgOfUsedFunctionInMockObject(mockedObject: Any, function: (T) -> S, clsOfArgument: Class<T>): T{
    val argCaptor= ArgumentCaptor.forClass(clsOfArgument)
    val ver = verify(mockedObject)
    argCaptor.capture()
    ver.javaClass.methods.first { it.name == function.reflect()!!.name }.invoke(ver, uninitialized())
    return argCaptor.value
}
private fun <T> uninitialized(): T = null as T

Usage: (Say I have mocked my repository and tested a viewModel. After calling the viewModel's "update()" method with a MenuObject object, I want to make sure that the MenuObject actually called upon the repository's "updateMenuObject()" method:

viewModel.update(menuObjectToUpdate)
val arg = getTheArgOfUsedFunctionInMockObject(mockedRepo, mockedRepo::updateMenuObject, MenuObject::class.java)
assertEquals(menuObjectToUpdate, arg)
Re'em
  • 1,869
  • 1
  • 22
  • 28
0

You can write a wrapper over argument captor

class CaptorWrapper<T:Any>(private val captor:ArgumentCaptor<T>, private val obj:T){
    fun capture():T{
        captor.capture()
        return obj
    }

    fun captor():ArgumentCaptor<T>{
        return captor
    }
}
Amit Kaushik
  • 642
  • 7
  • 13
0

Another approach:

/**
 * Use instead of ArgumentMatcher.argThat(matcher: ArgumentMatcher<T>)
 */
fun <T> safeArgThat(matcher: ArgumentMatcher<T>): T {
    ThreadSafeMockingProgress.mockingProgress().argumentMatcherStorage
        .reportMatcher(matcher)
    return uninitialized()
}

@Suppress("UNCHECKED_CAST")
private fun <T> uninitialized(): T = null as T

Usage:

verify(spiedElement, times(1)).method(
    safeArgThat(
        CustomMatcher()
    )
)
Nikolas
  • 1
  • 2
0

If none of the fine solutions presented worked for you, here is one more way to try. It's based on Mockito-Kotlin.

[app/build.gradle]
dependencies {
    ...
    testImplementation 'org.mockito.kotlin:mockito-kotlin:3.2.0'
}

Define Rule and Mock in your test file.

@RunWith(AndroidJUnit4::class)
class MockitoTest {
    @get:Rule
    val mockitoRule: MockitoRule = MockitoJUnit.rule()

    @Mock
    private lateinit var mockList: MutableList<String>

And here is an example.

    @Test
    fun `argument captor`() {
        mockList.add("one")
        mockList.add("two")

        argumentCaptor<String>().apply {
            // Verify that "add()" is called twice, and capture the arguments.
            verify(mockList, times(2)).add(capture())

            assertEquals(2, allValues.size)
            assertEquals("one", firstValue)
            assertEquals("two", secondValue)
        }
    }
}

Alternatively, you can also use @Captor as well.

@Captor
private lateinit var argumentCaptor: ArgumentCaptor<String>

@Test
fun `argument captor`() {
    mockList.add("one")
    mockList.add("two")

    verify(mockList, times(2)).add(capture(argumentCaptor))

    assertEquals(2, argumentCaptor.allValues.size)
    assertEquals("one", argumentCaptor.firstValue)
    assertEquals("two", argumentCaptor.secondValue)
}
solamour
  • 2,764
  • 22
  • 22