PSA, don’t construct new CoroutineScope
s 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.io
And now the previous example @Test
will fail as expected if transactionsDb.refreshTransactions()
throws an exception.