Skip to content

Instantly share code, notes, and snippets.

@RBusarow
Last active November 20, 2023 19:03
Show Gist options
  • Save RBusarow/70256d782e2d789cfa167a0163b2e22e to your computer and use it in GitHub Desktop.
Save RBusarow/70256d782e2d789cfa167a0163b2e22e to your computer and use it in GitHub Desktop.
A JUnit 4 Rule and JUnit 5 Extension for utilizing TestCoroutineDispatcher and TestCoroutineScope from kotlinx.coroutines-test
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.TestInstancePostProcessor
@ExtendWith(TestCoroutineExtension::class)
interface CoroutineTest {
var testScope: TestCoroutineScope
var dispatcher: TestCoroutineDispatcher
}
/**
* JUnit 5 Extension for automatically creating a [TestCoroutineDispatcher],
* then a [TestCoroutineScope] with the same CoroutineContext.
*
* [TestCoroutineScope.cleanupTestCoroutines] is called in afterEach
* instead of afterAll in case Lifecycle.PER_CLASS is selected,
* and will cause an exception if any coroutines are leaked.
*
* Usage of an extension in a Kotlin JUnit 5 test:
*
* class MyTest : CoroutineTest {
*
* override lateinit var testScope: TestCoroutineScope
* override lateinit var dispatcher: TestCoroutineDispatcher
* }
*
*/
@ExperimentalCoroutinesApi
class TestCoroutineExtension : TestInstancePostProcessor, BeforeAllCallback, AfterEachCallback, AfterAllCallback {
val dispatcher = TestCoroutineDispatcher()
val testScope = TestCoroutineScope(dispatcher)
override fun postProcessTestInstance(testInstance: Any?, context: ExtensionContext?) {
(testInstance as? CoroutineTest)?.let { coroutineTest ->
coroutineTest.testScope = testScope
coroutineTest.dispatcher = dispatcher
}
}
override fun beforeAll(context: ExtensionContext?) {
Dispatchers.setMain(dispatcher)
}
override fun afterEach(context: ExtensionContext?) {
testScope.cleanupTestCoroutines()
}
override fun afterAll(context: ExtensionContext?) {
Dispatchers.resetMain()
}
}
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import kotlin.coroutines.ContinuationInterceptor
/**
* JUnit 4 Rule for automatically creating a [TestCoroutineDispatcher],
* then a [TestCoroutineScope] with the same CoroutineContext.
*
* [TestCoroutineScope.cleanupTestCoroutines] is called after execution of each test,
* and will cause an exception if any coroutines are leaked.
*
* Usage of a rule in a Kotlin JUnit 4 test is:
*
* class MyTest {
*
* @get:Rule val testRule = TestCoroutineRule()
*
* val dispatcher = testRule.dispatcher
* }
*
*/
@ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule, TestCoroutineScope by TestCoroutineScope() {
val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(dispatcher)
// everything above this happens before the test
base.evaluate()
// everything below this happens after the test
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
}
}
@javadjafari1
Copy link

According to the new API changes in the kotlin coroutine, it should be something like this.

https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md

for Junit5

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.jupiter.api.extension.TestInstancePostProcessor

/**
 * JUnit 5 Extension for automatically creating a [UnconfinedTestDispatcher],
 * then a [TestScope] with the same CoroutineContext.
 * */
@ExperimentalCoroutinesApi
class TestCoroutineExtension : TestInstancePostProcessor, BeforeAllCallback, AfterAllCallback {

    private val dispatcher = UnconfinedTestDispatcher()
    private val testScope = TestScope(dispatcher)

    override fun postProcessTestInstance(testInstance: Any?, context: ExtensionContext?) {
        (testInstance as? CoroutineTest)?.let { coroutineTest ->
            coroutineTest.testScope = testScope
            coroutineTest.dispatcher = dispatcher
        }
    }

    override fun beforeAll(context: ExtensionContext?) {
        Dispatchers.setMain(dispatcher)
    }

    override fun afterAll(context: ExtensionContext?) {
        Dispatchers.resetMain()
    }
}

for Junit4

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.rules.TestWatcher
import org.junit.runner.Description

@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineRule(
    val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {

    override fun starting(description: Description) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description) {
        super.finished(description)
        Dispatchers.resetMain()
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment