Sharp Edges in Kotlin Coroutines Testing Tools
Article Summary
Kevin Cianfarini from Cash App discovered a sneaky threading bug that stumped his entire team. The culprit? A misunderstood feature in Kotlin's coroutine testing tools that silently breaks thread assertions.
While migrating Cash App's MVP architecture to Kotlin coroutines, the team hit a confusing wall: their tests were failing because TestCoroutineDispatcher wasn't behaving as expected. What seemed like a simple withContext() fix turned into a deep dive into how 'immediate' dispatchers actually work.
Key Takeaways
- TestCoroutineDispatcher's 'immediate' mode executes on current thread, not original dispatcher
- withContext() won't switch threads back when using unconfined or immediate dispatchers
- Thread assertions fail with TestCoroutineDispatcher due to its unconfined threading policy
- Solution: Assert on CoroutineDispatcher directly, not Thread objects in tests
TestCoroutineDispatcher's 'immediate' behavior means it won't respect thread switching in withContext(), breaking common testing assumptions about dispatcher behavior.
About This Article
Cash App's MVP architecture test expected Thread[main @coroutine#1,5,main] but got Thread[test,5,main]. The issue was that TestCoroutineDispatcher's immediate mode runs tasks on the current thread instead of switching dispatcher context as expected.
Kevin Cianfarini's team fixed this by replacing thread-level assertions with CoroutineDispatcher assertions using currentCoroutineContext()[CoroutineDispatcher.Key]. When thread verification was necessary, they switched to standard runBlocking instead.
The fix stopped silent test failures. It clarified that 'immediate' in coroutine dispatchers means skipping CoroutineDispatcher.dispatch calls entirely, not enabling manual virtual clock control. The team had misread the documentation initially.