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

Add a Comment

Your email address will not be published.