mockito / mockito-kotlin

Using Mockito with Kotlin
MIT License
3.1k stars 200 forks source link

How to use Mockito ArgumentCaptor to verify Lambda (Function Type) has been passed? #380

Closed annachen368 closed 4 years ago

annachen368 commented 4 years ago

Can someone educate me how to use mockito argumentCaptor to capture kotlin function type () -> Unit?

I want to verify if doSomething() is called in onPositiveButtonClicked() method.

I can't find any example from https://github.com/nhaarman/mockito-kotlin

This is the error:

"Argument(s) are different! Wanted: uiListener.showDialog( "Test", "This is a dialog.", (should show dialog and do something$1) Function0 ); -> at com.example.core.MainPresenterTest.should show dialog and do something$app_debug(MainActivityTest.kt:28) Actual invocation has different arguments: uiListener.showDialog( "Test", "This is a dialog.", (showDialogAndDoSomething$1) Function0 ); -> at com.example.core.MainPresenter.showDialogAndDoSomething(MainPresenter.kt:13)"

This is the test class: `class MainPresenterTest {

val presenter = MainPresenter()

val uiListener: UiListener = mock()

// this is successful
@Test
internal fun `should show dialog`() {
    presenter.showDialog(uiListener)

    verify(uiListener).showDialog(
        title = "Test",
        message = "This is a dialog."
    )
}

@Test
internal fun `should show dialog and do something`() {
    presenter.showDialogAndDoSomething(uiListener)

    val captor = argumentCaptor<() -> Unit>()

    verify(uiListener).showDialog(
        title = "Test",
        message = "This is a dialog.",
        onPositiveButtonClicked = {
            captor.capture()
        }
    )

    captor.firstValue.invoke() // test crashed here because captor.size is 0

    verify(presenter, times(1)).doSomething(uiListener)
}

}`

My class looks like this:

class MainPresenter {

    fun showDialog(uiListener: UiListener) {
        uiListener.showDialog(
            title = "Test",
            message = "This is a dialog."
        )
    }

    fun showDialogAndDoSomething(uiListener: UiListener) {
        uiListener.showDialog(
            title = "Test",
            message = "This is a dialog.",
            onPositiveButtonClicked = {
                doSomething(uiListener)
            }
        )
    }

    fun doSomething(uiListener: UiListener) {
        uiListener.log("hey")
    }
}

interface UiListener {

    fun showDialog(title: String, message: String, onPositiveButtonClicked: () -> Unit = {})

    fun log(message: String)
}
bohsen commented 4 years ago

Use a Matcher in your verification. Like this:

verify(uiListener).showDialog(
        eq("Test"),
       eq("This is a dialog.")
    )

For the second test that you say fails, I would verify that log was invoked on the mock UiListener as this verify(presenter, times(1)).doSomething(uiListener) would require you to use a Spy on your presenter.

And just for the cause of it. Watch out with mocking. You tie your tests to the implementation details. This can make your code a nightmare to maintain and you might end up using more time maintaining tests that actual code.

annachen368 commented 4 years ago

Use a Matcher in your verification. Like this:

verify(uiListener).showDialog(
        eq("Test"),
       eq("This is a dialog.")
    )

For the second test that you say fails, I would verify that log was invoked on the mock UiListener as this verify(presenter, times(1)).doSomething(uiListener) would require you to use a Spy on your presenter.

And just for the cause of it. Watch out with mocking. You tie your tests to the implementation details. This can make your code a nightmare to maintain and you might end up using more time maintaining tests that actual code.

Got some errors:

` org.mockito.exceptions.misusing.InvalidUseOfMatchersException: Invalid use of argument matchers! 3 matchers expected, 2 recorded: -> at com.nhaarman.mockitokotlin2.MatchersKt.eq(Matchers.kt:34) -> at com.nhaarman.mockitokotlin2.MatchersKt.eq(Matchers.kt:34)

This exception may occur if matchers are combined with raw values: //incorrect: someMethod(anyObject(), "raw String"); When using matchers, all arguments have to be provided by matchers. For example: //correct: someMethod(anyObject(), eq("String by matcher"));

For more info see javadoc for Matchers class.

at com.example.core.UiListener$DefaultImpls.showDialog$default(UiListener.kt:5)
at com.example.core.MainPresenterTest.should show dialog and do something$app_debug(MainActivityTest.kt:66)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
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.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
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.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Argument(s) are different! Wanted: uiListener.showDialog( "Test", "This is a dialog.", (should show dialog$1) Function0 ); -> at com.example.core.MainPresenterTest.should show dialog$app_debug(MainActivityTest.kt:24) Actual invocation has different arguments: uiListener.showDialog( "Test", "This is a dialog.", (showDialog$1) Function0 ); -> at com.example.core.MainPresenter.showDialog(MainPresenter.kt:6)

Comparison Failure:

Argument(s) are different! Wanted: uiListener.showDialog( "Test", "This is a dialog.", (should show dialog$1) Function0 ); -> at com.example.core.MainPresenterTest.should show dialog$app_debug(MainActivityTest.kt:24) Actual invocation has different arguments: uiListener.showDialog( "Test", "This is a dialog.", (showDialog$1) Function0 ); -> at com.example.core.MainPresenter.showDialog(MainPresenter.kt:6) at com.example.core.MainPresenterTest.should show dialog$app_debug(MainActivityTest.kt:24) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) 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.runners.ParentRunner.runLeaf(ParentRunner.java:325) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57) 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.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) Process finished with exit code 255`
bohsen commented 4 years ago

Sry, I didn't notice the parentheses around your argumentCaptor'er. You should do this:

@Test
fun shouldShowDialogAndDoSomething() {
    presenter.showDialogAndDoSomething(uiListener)
    val captor = argumentCaptor<(UiListener) -> Unit>()

    verify(uiListener).showDialog(
            eq("Test"),
            eq("This is a dialog."),
            captor.capture()
    )

    captor.firstValue(uiListener)

    verify(uiListener, times(1)).log(any())
}

Also you where missing an argument in the argumentCaptor

annachen368 commented 4 years ago

Thanks bohsen!