
Simplifies replacing real CoroutineDispatchers with TestDispatchers by passing a DispatcherProvider through CoroutineContext; includes runTest/TestScope integrations, default dispatcher-to-test mapping and lint rules.
Lightweight KMP library to simplify replacing real CoroutineDispatcher instances with TestDispatcher by passing around a DispatcherProvider instance through the CoroutineContext.
Suspending methods can select the dispatcher to use with the top-level currentDispatchers() method that the library provides. In test code you just have to replace the kotlinx.coroutines.test.runTest import with inc.dna.coroutines.test.runTest.
class MyUseCase {
suspend operator fun invoke() = withContext(currentDispatchers().io) {
}
}import inc.dna.coroutines.test.runTest
class MyUseCaseTest {
fun `my test`() = runTest {
// Arrange
val subject = MyUseCase()
// Act
subject.invoke()
// Assert
}
}For classes that have a constructor injected scope, like ViewModels you have to make sure that you pass in a scope which is either the TestScope, backgroundScope or a custom scope that inherits the DispatchersContextElement from the TestScope.coroutineContext.
Then you can use the extension property on val CoroutineScope.dispatchers to select the dispatcher that you need.
class MyViewModel(
val scope: CoroutineScope,
) : ViewModel(scope) {
private val defaultDispatcher = scope.dispatchers.default
}import inc.dna.coroutines.test.runTest
class MyViewModelTest {
@Test
fun `my test`() = runTest {
val subject = MyViewModel(
scope = backgroundScope
)
}
}If a class creates a CoroutineScope internally it should either use a constructor-injected CoroutineContext or a constructor-injected parent scope to build upon to ensure that the scope inherits the DispatchersContextElement.
class MySelfContainedClass(
val context: CoroutineContext = EmptyCoroutineContext,
) {
val scope = CoroutineScope(context + SupervisorJob(parent = context.job) + context.dispatchers.default)
fun release() {
scope.cancel()
}
}import inc.dna.coroutines.test.runTest
class MyViewModelTest {
@Test
fun `my test`() = runTest {
// Arrange
val subject = MySelfContainedClass(
scope = coroutineContext
)
subject.release()
}
}Besides the top-level runTest, the coroutines-test artifact also allows early creation of the TestScope so that you can use it to instantiate dependencies when the test framework creates your test instance.
For this the library provides the inc.dna.coroutines.test.TestScope top-level factory method which ensures that the TestScope is instantiated with the right CoroutineContext elements.
import inc.dna.coroutines.test.TestScope
class MyViewModelTest {
val scope = TestScope()
val subject = MySelfContainedClass(scope.backgrounScope)
@Test
fun `my test`() = scope.runTest {
...
}
}The library works by making the CoroutineContext of the runTest method contain a DispatcherContextElement which production code will look for. If it can't find the element in the current CoroutineContext real dispatchers will be used.
This means that it is important to structure your code in such a way that all scopes that are used or created during test execution inherit their CoroutineContext from the test, which should in any case be done because of structure concurrency rules.
The library has uses a default mapping for mapping real dispatchers to either StandardTestDispatcher or UnconfinedTestDispatcher according to the following mapping table
| Prod | Test | |
|---|---|---|
dispatches.default |
Dispatchers.Default |
StandardTestDispatcher |
dispatches.io |
Dispatchers.IO |
StandardTestDispatcher |
dispatches.main |
Dispatchers.Main |
UnconfinedTestDispatcher |
dispatches.mainImmediate |
Dispatchers.Main.immediate |
UnconfinedTestDispatcher |
dispatches.unconfined |
Dispatchers.Unconfined |
UnconfinedTestDispatcher |
This behavior can be overridden using an extension method on the DispatcherProvider interface that is part of the dispatchers-test artifact.
fun `my test`() = runTest {
dispatchers.setAll(::UnconfinedTestDispatcher)
dispatchers.set(DispatcherId.IO, ::UnconfinedTestDispatcher)
..
}dependencies {
implementation("inc.dna.coroutines:dispatchers:<latest-version>")
testImplementation("inc.dna.coroutines:dispatchers-test:<latest-version>")
}For KMP projects
kotlin {
sourceSets {
commonMain {
implementation("inc.dna.coroutines:dispatchers:<latest-version>")
}
commonTest {
implementation("inc.dna.coroutines:dispatchers-test:<latest-version>")
}
}
}The library ships lint rules which will flag the usages of the following types in favor of the equivalents from this library.
kotlinx.coroutines.Dispatcherskotlinx.coroutines.test.runTestkotlinx.coroutines.test.TestScopeTo come
Detekt might be preferred over lint because it tends to run quicker so there will be detekt rules coming as well
Lightweight KMP library to simplify replacing real CoroutineDispatcher instances with TestDispatcher by passing around a DispatcherProvider instance through the CoroutineContext.
Suspending methods can select the dispatcher to use with the top-level currentDispatchers() method that the library provides. In test code you just have to replace the kotlinx.coroutines.test.runTest import with inc.dna.coroutines.test.runTest.
class MyUseCase {
suspend operator fun invoke() = withContext(currentDispatchers().io) {
}
}import inc.dna.coroutines.test.runTest
class MyUseCaseTest {
fun `my test`() = runTest {
// Arrange
val subject = MyUseCase()
// Act
subject.invoke()
// Assert
}
}For classes that have a constructor injected scope, like ViewModels you have to make sure that you pass in a scope which is either the TestScope, backgroundScope or a custom scope that inherits the DispatchersContextElement from the TestScope.coroutineContext.
Then you can use the extension property on val CoroutineScope.dispatchers to select the dispatcher that you need.
class MyViewModel(
val scope: CoroutineScope,
) : ViewModel(scope) {
private val defaultDispatcher = scope.dispatchers.default
}import inc.dna.coroutines.test.runTest
class MyViewModelTest {
@Test
fun `my test`() = runTest {
val subject = MyViewModel(
scope = backgroundScope
)
}
}If a class creates a CoroutineScope internally it should either use a constructor-injected CoroutineContext or a constructor-injected parent scope to build upon to ensure that the scope inherits the DispatchersContextElement.
class MySelfContainedClass(
val context: CoroutineContext = EmptyCoroutineContext,
) {
val scope = CoroutineScope(context + SupervisorJob(parent = context.job) + context.dispatchers.default)
fun release() {
scope.cancel()
}
}import inc.dna.coroutines.test.runTest
class MyViewModelTest {
@Test
fun `my test`() = runTest {
// Arrange
val subject = MySelfContainedClass(
scope = coroutineContext
)
subject.release()
}
}Besides the top-level runTest, the coroutines-test artifact also allows early creation of the TestScope so that you can use it to instantiate dependencies when the test framework creates your test instance.
For this the library provides the inc.dna.coroutines.test.TestScope top-level factory method which ensures that the TestScope is instantiated with the right CoroutineContext elements.
import inc.dna.coroutines.test.TestScope
class MyViewModelTest {
val scope = TestScope()
val subject = MySelfContainedClass(scope.backgrounScope)
@Test
fun `my test`() = scope.runTest {
...
}
}The library works by making the CoroutineContext of the runTest method contain a DispatcherContextElement which production code will look for. If it can't find the element in the current CoroutineContext real dispatchers will be used.
This means that it is important to structure your code in such a way that all scopes that are used or created during test execution inherit their CoroutineContext from the test, which should in any case be done because of structure concurrency rules.
The library has uses a default mapping for mapping real dispatchers to either StandardTestDispatcher or UnconfinedTestDispatcher according to the following mapping table
| Prod | Test | |
|---|---|---|
dispatches.default |
Dispatchers.Default |
StandardTestDispatcher |
dispatches.io |
Dispatchers.IO |
StandardTestDispatcher |
dispatches.main |
Dispatchers.Main |
UnconfinedTestDispatcher |
dispatches.mainImmediate |
Dispatchers.Main.immediate |
UnconfinedTestDispatcher |
dispatches.unconfined |
Dispatchers.Unconfined |
UnconfinedTestDispatcher |
This behavior can be overridden using an extension method on the DispatcherProvider interface that is part of the dispatchers-test artifact.
fun `my test`() = runTest {
dispatchers.setAll(::UnconfinedTestDispatcher)
dispatchers.set(DispatcherId.IO, ::UnconfinedTestDispatcher)
..
}dependencies {
implementation("inc.dna.coroutines:dispatchers:<latest-version>")
testImplementation("inc.dna.coroutines:dispatchers-test:<latest-version>")
}For KMP projects
kotlin {
sourceSets {
commonMain {
implementation("inc.dna.coroutines:dispatchers:<latest-version>")
}
commonTest {
implementation("inc.dna.coroutines:dispatchers-test:<latest-version>")
}
}
}The library ships lint rules which will flag the usages of the following types in favor of the equivalents from this library.
kotlinx.coroutines.Dispatcherskotlinx.coroutines.test.runTestkotlinx.coroutines.test.TestScopeTo come
Detekt might be preferred over lint because it tends to run quicker so there will be detekt rules coming as well