Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
104: Testing Coroutines with runTest
If you've tried testing coroutines using runBlocking, you've probably noticed that your test suite starts feeling sluggish the moment you add a delay(). It's frustrating because you're essentially forcing your CPU to actually sit there and wait for real-world time to pass just to verify a piece of logic. That's where runTest comes in.
Why can't I just keep using runBlocking?
The short answer is: runBlocking is for bridging the gap between non-coroutine code and coroutines; it isn't designed for testing. When you use runBlocking, a delay(1000) actually takes one full second. Now, one second doesn't seem like much, but once you have 500 tests each with a few delays, your CI pipeline becomes a nightmare.
runTest is different because it uses a TestCoroutineScheduler. It implements something called "virtual time." When the code hits a delay, runTest doesn't actually make the thread sleep. Instead, it just fast-forwards the virtual clock to the point where the delay ends. It's essentially a time machine for your tests.
// Imagine a service that fetches weather data with a simulated network lag class WeatherService { suspend fun fetchTemperature(): String { delay(5000) // Simulate a slow API return "22°C" } } // Using runTest, this finishes in milliseconds, not 5 seconds @Test fun `test fetchTemperature returns correct value`() = runTest { val service = WeatherService() val result = service.fetchTemperature() assertEquals("22°C", result) }How do I test things that happen "eventually" without waiting?
Sometimes you aren't just waiting for a single return value; you're waiting for a background task to complete or a state to change. If you launch a coroutine inside your test,
runTestwill automatically wait for all children to finish before completing. But what if you need to check the state during that process?I usually rely on
advanceTimeBy(ms)oradvanceUntilIdle(). These let you surgically move the clock forward to see if your code is behaving correctly at specific intervals. I'll show you with a simple "Debounce" example—like a search bar that only triggers after the user stops typing.@Test fun `search query should only trigger after debounce period`() = runTest { val searchViewModel = SearchViewModel() // assume it has a 300ms debounce searchViewModel.onQueryChanged("K") advanceTimeBy(200) assertEquals(0, searchViewModel.requestCount) // Too early! advanceTimeBy(101) // Now we've hit 301ms assertEquals(1, searchViewModel.requestCount) // Triggered! }Why does my test crash with "Module with Main dispatcher had failed to initialize"?
This is the classic "Welcome to Kotlin Testing" error.
Dispatchers.Mainis usually tied to a platform-specific main thread (like the Android UI thread). In a JUnit test, that main thread doesn't exist. Even if yourrunTestblock is working fine, if the code inside your class callswithContext(Dispatchers.Main), it'll blow up.You have to tell Kotlin to replace the Main dispatcher with a test dispatcher. The cleanest way to do this is by creating a JUnit Rule or using a
@Beforemethod to set the main dispatcher. I personally prefer a Rule because it handles the cleanup automatically, but the manual way looks like this:private val testDispatcher = StandardTestDispatcher() @Before fun setup() { Dispatchers.setMain(testDispatcher) } @After fun tearDown() { Dispatchers.resetMain() }By doing this, whenever your production code asks for
Dispatchers.Main, it gets yourtestDispatcherinstead, which is fully compatible with the virtual time skipping ofrunTest.What if my code uses a custom scope that I can't control?
If your class creates its own
CoroutineScope(Dispatchers.IO)internally,runTestloses its power. Why? BecauserunTestonly controls coroutines launched within its own context or using the provided test scheduler. A hardcodedDispatchers.IOcall escapes that sandbox and runs on a real thread with real time.The fix is simple: Inject your dispatchers. Don't hardcode
Dispatchers.IOinside your classes. Pass the dispatcher as a constructor argument. In production, you pass the real one; in tests, you pass yourtestDispatcher. Trust me, this is one of those "best practices" that actually saves you hours of debugging flaky tests later on.
There are no comments for now.