Avoid using `runBlocking` in tests

Is there a way to avoid using runBlocking in tests?

I currently have the following:

class ServerTest {

  @Test fun testLocalServer() = runBlocking {
	...
  }

  @Test fun testStagedServer() = runBlocking {
	...
  }
}

I would like this to somehow become:

  @Test fun testLocalServer() {
	...
  }

  @Test fun testStagedServer()  {
	...
  }

Basically, I want my code to look like the second code block but still behave exactly like the first code block. I am assuming that I will want every single one of my tests to run inside of a runBlocking block, so it’s redundant to keep writing it.

Make sure to include testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
Sadly, @Test suspend fun is not planned to be supported. Instead, you’re meant to use runTest instead of runBlocking

2 Likes

Thanks @kyay10 !

1 Like

@mjgroth I know I’m late to the party, but depending on which test library you are using - the answer is maybe. Thought I’d post in case anyone found this useful in the future. Documentation can be found here: JUnit 5 User Guide

For example, using JUnit5, with test lifecycles and extensions, what you’re trying to achieve is possible. The way of achieving this is done as shown below. Contextually, the reason I originally wanted to do this, was because I had the following scenario:

  1. I had a ‘long’ polling function, which may or may not finish by the time the test ends, depending on whether a developer wrapped their test with runTest (primarily think of unexpected test failures, but also other reasons)
  2. I noticed often myself, or other colleagues, may forget to wrap the test function with a runTest
  3. This meant under certain circumstances, memory leaks were introduced, due to a number of factors, and I needed a way of protecting myself from doing something dumb

Please keep in mind, you also may need to do this for each different lifecycle method too, as extra protection (ie the same situation could happen in the beforeAll, beforeEach, afterEach, afterAll, of a test, and you’d be in the same position).

The other thing to note is that depending on how/if you’re running your test in parallel, there may be other considerations you need to factor in (one example shows below the edge case of an exception, and how you atomically handle that)

import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.InvocationInterceptor
import org.junit.jupiter.api.extension.ReflectiveInvocationContext
import java.lang.reflect.Method
import java.util.concurrent.atomic.AtomicReference

/**
 * Wraps every test in runTest, to ensure you don't need
 * to remember to do this each time, in case a developer
 * forgets and accidentally causes memory leaks. This does
 * NOT cater for handling of lifecycle methods, where the same
 * thing may happen
 */
class TestInvocationListener: InvocationInterceptor {

    @Throws(Throwable::class)
    override fun interceptTestMethod(
        invocation: InvocationInterceptor.Invocation<Void>,
        invocationContext: ReflectiveInvocationContext<Method>,
        extensionContext: ExtensionContext,
    ) {
        val throwable = AtomicReference<Throwable>()
        println("about to wrap test")

        runTest {
            try {
                println("About to call test invocation")
                invocation.proceed()
            } catch (t: Throwable) {
                throwable.set(t)
            }

            // caters for the behaviour where we need to re-throw the exception
            val t = throwable.get()
            if (t != null) {
                throw t
            }
        }
    }
}

Now, you get your behaviour for free and don’t need to worry about it :). Hope this helps, even months after the question was asked.

Thank you for the detailed answer @npnk !

Lately I have realized that runTest is pretty much unavoidable if you are developing kotlin multiplatform, because Kotlin JS currently depends on it (and there seems to be no realistic alternative, JS tests do not currently even start without runTest).

Since I want to try to run my tests in Kotlin Common modules that may target JVM, Android, and JS, I am realizing that for now I have to be comfortable with doing tests the idiomatic way (with runTest.

1 Like

Sorry for responding in this thread a bit late, but I need help,
my scenario is a bit different since I Changed to runTest but here it is.

When I run the test individually, it passes, however when I run my tests collectively, it fails for the release variant
i.e ./gradlew test fails, ./gradlew :app:testS***DebugUnitTest, this passed

The exception thrown is

Execution failed for task ':app:testS**ReleaseUnitTest'.
Caused by: java.lang.ClassNotFoundException at CustomerProfileViewModelTest.kt:87

Here is the code block it’s complaining about

 @Test
    fun `sort FlowData by date`() {
        Locale.setDefault(Locale.ENGLISH)

        val customerId = 1
        runTest {
            val salesCase0 = createCustomerSalesCase(closedAt = "2019-06-01T00:00:00.000Z")
            val salesCase1 = createCustomerSalesCase(closedAt = "2019-06-03T00:00:00.000Z")
            val salesCase2 = createCustomerSalesCase(closedAt = "2019-06-02T00:00:00.000Z")
            val salesCase3 = createCustomerSalesCase(closedAt = "2019-06-04T00:00:00.000Z")
            val unsortedSalesCaseList = listOf(
                salesCase0, salesCase1, salesCase2,
                salesCase3
            )

            whenever(profileDataUseCase.getSalesCasesByRemoteCustomerId(RemoteCustomerId(customerId))).thenReturn(
                unsortedSalesCaseList.right()
            )

            val viewModel = CustomerProfileViewModel(profileDataUseCase, coroutineContext)
            viewModel.initWithCustomerId(RemoteCustomerId(customerId))

            viewModel.customerProfileLiveData.observeForever(salesCasesObserver)

            viewModel.loadProfileData()
        }

        argumentCaptor<CustomerProfileState> {
            verify(salesCasesObserver).onChanged(capture())
            val result = firstValue as CustomerProfileState.Success

            "2019-06-04T00:00:00.000Z" `should be equal to` result.salesCases[0].closedAt
            "2019-06-03T00:00:00.000Z" `should be equal to` result.salesCases[1].closedAt
            "2019-06-02T00:00:00.000Z" `should be equal to` result.salesCases[2].closedAt
            "2019-06-01T00:00:00.000Z" `should be equal to` result.salesCases[3].closedAt
        }
    }
}

Which line is line 87? What is the class that is not found?

You should probably check your dependencies in your gradle build file and make sure there isn’t anything that’s only included in debug builds.