PSA, don’t construct new CoroutineScopes and break structured concurrency in your classes.
class TransactionsDb(dispatchers: MyDispatchers) {
private val scope = CoroutineScope(dispatchers.io)
}This can cause tests to pass when an exception is thrown.
@Test
fun loadTransactionsDbTest() = runBlocking<Unit> {
val testScope: CoroutineScope = this
val transactionsDb = TransactionsDb(
_scope = testScope,
dispatchers = testScope.asDispatchers()
)
// if this function throws an exception, the test will pass because the new scope constructed internally
// in `TransactionsDb` will swallow the exception and prevent it from propagating to the `runBlocking { }`
transactionsDb.refreshTransactions()
}Instead, you should inject a CoroutineScope that hopefully comes from an object graph (ex: MyFeatureGraph).
class MyFeatureGraph {
val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
val dispatchers = MyDispatchers()
val transactionsDb: TransactionsDb = TransactionsDb(scope, dispatchers)
}If you wish to change the dispatcher used by TransactionsDb, you can do something like the following and maintain structured concurrency:
class TransactionsDb(
_scope: CoroutineScope,
dispatchers: MyDispatchers
) : CoroutineScope by _scope + dispatchers.ioAnd now the previous example @Test will fail as expected if transactionsDb.refreshTransactions() throws an exception.