How to inject the coroutines Dispatchers into your testable code
Best practices for coroutines say that you should always inject the coroutines Dispatchers. It will allow you to control your Dispatchers during your tests. You can read more on the official Android Guidelines.
https://developer.android.com/kotlin/coroutines/coroutines-best-practices#inject-dispatchers
I will show you my approach to this problem, and how I solve it. There will be a lot of code examples, code tells more than a thousand words.
I always create interface Dispatcher Provider where I also store the default implementation:
interface DispatcherProvider {
val main
get() = Dispatchers.Main
val default
get() = Dispatchers.Default
val io
get() = Dispatchers.IO
val unconfined
get() = Dispatchers.Unconfined
}
class DefaultDispatcherProvider : DispatcherProvider
Usage of it in production code is very simple:
class MyViewModel(
private val dispatchers: DispatcherProvider = DefaultDispatcherProvider()
) : ViewModel() {
fun tryDispatcher() {
viewModelScope.launch(dispatchers.io) {
// IO stuff here
withContext(dispatchers.main){
// Main thread stuff here
}
}
}
}
Then in test source I have test implementation:
class TestDispatcherProvider(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : DispatcherProvider {
override val default
get() = testDispatcher
override val io
get() = testDispatcher
override val main
get() = testDispatcher
override val unconfined
get() = testDispatcher
}
Usage of it is little Usage of it is a little more complicated, but not much. I use JUnit5 and its extension mechanism, but everything can be easily translated into the JUnit4 test Rule.
class TestDispatcherExtension(
val testDispatchers: TestDispatcherProvider = TestDispatcherProvider()
) : BeforeEachCallback, AfterEachCallback, TestCoroutineScope by TestCoroutineScope(testDispatchers.testDispatcher) {
override fun beforeEach(context: ExtensionContext?) {
Dispatchers.setMain(testDispatchers.testDispatcher)
}
override fun afterEach(context: ExtensionContext?) {
Dispatchers.resetMain()
testDispatchers.testDispatcher.cleanupTestCoroutines()
}
}
In tests you just need to register an extension and use it:
class ExampleTest {
@JvmField @RegisterExtension
val testDispatcherExtension = TestDispatcherExtension()
private val viewModel = MyViewModel(
dispatchers = testDispatcherExtension.testDispatchers
)
fun `Given When Then test` = testDispatcherExtension.runTest {
//do test stuff here
}
}
And thatβs all! I showed you how I create a simple DispatcherProvider for injection.
What is your approach for testing coroutines and injecting the Dispatchers?
Have a great day!
See you on The Code Side
Artur Latoszewski