1

I have something like the following in Kotlin:

class A {
    fun a(): Unit = TODO()
}
fun foo(a: A) {
    // Do something
    CoroutineScope(Dispatchers.IO).launch { 
      delay(1000)
      a.a() 
    }
    // Do something else
}

I want to write a test that verifies that the method A.a() was called by foo():

class MyTest {
  @Test
  fun `weird test`() {
    val mockA: A = mockk(relaxed = true)

    foo(mockA)

    verify(exactly = 1) { mockA.a() }
  }
}

Clearly, the test is failing because it is not synchronized with the coroutine. I tried to use runTest, but foo() is not a suspending function, and it is hiding the creation of the scope inside it.

What can I do?

6
  • 1
    Please edit the question to fix the compile errors. Make sure the code you provide can be executed. Commented Sep 3 at 21:18
  • 1
    Thanks for the edit. Although the TODO() produces an error, that only lets the coroutine fail. The test itself is successfully completed, without any failures. This is all expected, the code you provided does not reproduce the problem you describe, Please edit again to either provide code that matches the behavior you describe or clarify what issues you have with the current code, since it does work. -- Btw., you should either make the function suspending or pass it a CoroutineScope. Otherwise you cannot control the coroutine from the outside, which is usually necessary. Commented Sep 4 at 7:54
  • 1
    @tyg is right, the test passes. @riccardo.cardin, if you add delay(1000) before a.a() then the test will fail due to the issue that you've described. I've made changes in the answer Commented Sep 4 at 10:24
  • 1
    @riccardo.cardin, the idea is good, but it won’t work because A.a() is mocked, so delay(1000) never runs. You need to use a spy for A or go with my earlier suggestion Commented Sep 4 at 12:52
  • 1
    @riccardo.cardin, I realised that using spy won't help here. I've updated the question to make the code reflect the issue you described. I hope you don't mind. Also I tried to explain my changes in the edit message. Just to note - a downvote appeared before my changes. Commented Sep 4 at 21:36

2 Answers 2

2

Pass CoroutineDispatcher to the tested method instead of creating them inside the tested method.

You can create a controllable CoroutineDispatcher - StandardTestDispatcher(this.testScheduler) using coroutineScope provided by runTest.
And explicitly complete all coroutines on the test dispatcher - advanceUntilIdle() before running your verifications or assertions.

class MyTest {
    @Test
    fun `weird test`() = runTest {
        val mockA: A = mockk(relaxed = true)

        foo(mockA, StandardTestDispatcher(this.testScheduler))

        advanceUntilIdle()
        verify(exactly = 1) { mockA.a() }
    }
}

class A {
    fun a(): Unit = TODO()
}

fun foo(a: A, dispatcher: CoroutineDispatcher) {
    // Do something
    CoroutineScope(dispatcher).launch {
        delay(1000)
        a.a()
    }
    // Do something else
}

Also you can pass CoroutineScope instead of CoroutineDispatcher.
But note that CoroutineScope gives access to coroutines lifecycle (e.g. it could be used to cancel coroutines)

So:

  • If the caller needs to control the lifecycle of coroutines launched by the function, then pass a CoroutineScope to the function.
  • If the function should not expose direct access to its own coroutine lifecycle, then pass a CoroutineDispatcher and create CoroutineScope inside the function.

The issue usually addressed in Android development:

Sign up to request clarification or add additional context in comments.

2 Comments

Since I need to pass the dispatcher to the function foo, why not pass an instance of the CoroutineScope? Then, I can use TestScope.
Good question! Short answer - you can pass the CoroutineScope. I'll elaborate inside the answer
2

Use Awaitility to poll until the async launch finishes, then assert:

Assuming you use Gradle with Kotlin DSL:

testImplementation("org.awaitility:awaitility-kotlin:4.3.0")

Then do:

class MyTest {
    @Test
    fun `weird test`() {
        val mockA: A = mockk()

        foo(mockA)

        await.atMost(Duration.ofSeconds(1)).untilAsserted {
            verify(exactly = 1) { mockA.a() }
        }
    }
}

However injecting a Dispatcher is a better way to do this as mentioned in this answer.

2 Comments

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
Thanks for your answer. I know awaitility, but it's not idiomatic. IMO, it's a workaround when used together with coroutines. By the way, +1 because I didn't mention I want an idiomatic solution.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.