Elye
Elye

Reputation: 60131

How to unit test function that has coroutine `GlobalScope.launch`

I have this function

    override fun trackEvent(trackingData: TrackingData) {
        trackingData.eventsList()
    }

And I could have my test as below.

    @Test
    fun `My Test`() {
        // When
        myObject.trackEvent(myTrackingMock)

        // Then
        verify(myTrackingMock, times(1)).eventsList()
    }

However, if I make it into a

    override fun trackEvent(trackingData: TrackingData) {
        GlobalScope.launch{
            trackingData.eventsList()
        }
    }

How could I still get my test running? (i.e. can make the launch Synchronous?)

Upvotes: 10

Views: 4259

Answers (3)

Marko Topolnik
Marko Topolnik

Reputation: 200168

To approach the answer, try asking a related question: "How would I unit-test a function that has

Thread { trackingData.eventsList() }

in it?"

Your only hope is running a loop that repeatedly checks the expected condition, for some period time, until giving up and declaring the test failed.

When you wrote GlobalScope.launch, you waived your interest in Kotlin's structured concurrency, so you'll have to resort to unstructured and non-deterministic approaches of testing.

Probably the best recourse is to rewrite your code to use a scope under your control.

Upvotes: 3

tonisives
tonisives

Reputation: 2508

I refactored my method to

suspend fun deleteThing(serial: String): String? = coroutineScope {

This way, I can launch coroutines with launch

val jobs = mutableListOf<Job>()
var certDeleteError: String? = null

certs.forEach { certArn ->
    val job = launch {
        deleteCert(certArn, serial)?.let { error ->
            jobs.forEach { it.cancel() }
            certDeleteError = error
        }
    }
    jobs.add(job)
}

jobs.joinAll()

For the test, I can then just use runTest and it runs all of the coroutines synchronously

    @Test
    fun successfullyDeletes2Certs() = runTest {
        aws.deleteThing("s1")

Now you just need to mind your context where you are calling the deleteThing function. For me, it was a ktor request, so I could just call launch there also.

    delete("vehicles/{vehicle-serial}/") {
        launch {
            aws.deleteThing(serial)
        }
    }

Upvotes: 0

Elye
Elye

Reputation: 60131

I created my own CoroutineScope and pass in (e.g. CoroutineScope(Dispatchers.IO) as a variable myScope)

Then have my function

    override fun trackEvent(trackingData: TrackingData) {
        myScope.launch{
            trackingData.eventsList()
        }
    }

Then in my test I mock the scope by create a blockCoroutineScope as below.

   class BlockCoroutineDispatcher : CoroutineDispatcher() {
        override fun dispatch(context: CoroutineContext, block: Runnable) {
            block.run()
        }
    }

    private val blockCoroutineScope = CoroutineScope(BlockCoroutineDispatcher())

For my test, I'll pass the blockCoroutineScope in instead as myScope. Then the test is executed with launch as a blocking operation.

Upvotes: 4

Related Questions