I want to write tests for my android application. Sometimes the viewModel executes tasks in the background using Kotlins coroutine launch function. These tasks are executed in the viewModelScope that the androidx.lifecycle library so handily provides. In order to still test these functions, I replaced the default android Dispatchers with Dispatchers.Unconfined, which runs the code synchronously.
At least in most of the cases. When using suspendCoroutine, the Dispatchers.Unconfined will not be suspended and later resumed, but instead will simply return. The documentation of Dispatchers.Unconfined reveals why:
[Dispatchers.Unconfined] lets the coroutine resume in whatever thread that is used by the corresponding suspending function.
So by my understanding, the coroutine is not actually suspended, but the rest of the async function after suspendCoroutine is run on the thread that calls continuation.resume. Therefore the test fails.
Example:
class TestClassTest {
var testInstance = TestClass()
@Test
fun `Test with default dispatchers should fail`() {
testInstance.runAsync()
assertFalse(testInstance.valueToModify)
}
@Test
fun `Test with dispatchers replaced by Unconfined should pass`() {
testInstance.DefaultDispatcher = Dispatchers.Unconfined
testInstance.IODispatcher = Dispatchers.Unconfined
testInstance.runAsync()
assertTrue(testInstance.valueToModify)
}
@Test
fun `I need to also test some functions that use suspend coroutine - How can I do that?`() {
testInstance.DefaultDispatcher = Dispatchers.Unconfined
testInstance.IODispatcher = Dispatchers.Unconfined
testInstance.runSuspendCoroutine()
assertTrue(testInstance.valueToModify)//fails
}
}
class TestClass {
var DefaultDispatcher: CoroutineContext = Dispatchers.Default
var IODispatcher: CoroutineContext = Dispatchers.IO
val viewModelScope = CoroutineScope(DefaultDispatcher)
var valueToModify = false
fun runAsync() {
viewModelScope.launch(DefaultDispatcher) {
valueToModify = withContext(IODispatcher) {
sleep(1000)
true
}
}
}
fun runSuspendCoroutine() {
viewModelScope.launch(DefaultDispatcher) {
valueToModify = suspendCoroutine {
Thread {
sleep(1000)
//long running operation calls listener from background thread
it.resume(true)
}.start()
}
}
}
}
I was experimenting around with runBlocking, however it only helps if you call launch using the CoroutineScope created by runBlocking. But since my code is launching on the CoroutineScope provided by viewModelScope this will not work. If possible I would prefer not to inject a CoroutineScope everywhere, because any lower level class could (and does) make their own CoroutineScope as in the example, and then the Test has to know a lot about the implementation details. The idea behind the scopes is that every class has control over cancelling their asynchronous operations. The viewModelScope for example is by default cancelled when the viewModel is destroyed.
My question:
What coroutine dispatcher could I use that runs launch and withContext etc blocking (like Dispatchers.Unconfined) and also runs suspendCoroutine blocking?