What is runTest and why do you use it to test suspend functions instead of runBlocking?
answer
- kotlinx-coroutines-test artifact
- TestScope + TestCoroutineScheduler virtual clock
- delay() skipped, not really waited
- auto-advances children to idle
- rethrows uncaught child exceptions
basics
~10 srunTest is a helper from kotlinx-coroutines-test that runs a suspend test body. It skips delays using a fake clock, so tests with delay() finish instantly instead of really waiting.
solid answer
~40 srunTest is the entry point in kotlinx-coroutines-test for testing suspend code. It builds a TestScope backed by a TestCoroutineScheduler whose virtual clock makes delay() return immediately instead of suspending in real time, so a test that would sleep 10 seconds finishes in milliseconds. It auto-advances the scheduler until all coroutines launched in its scope are idle, then completes. By contrast runBlocking uses a real dispatcher and a real clock, so delays actually block the thread. runTest also aggregates uncaught exceptions from child coroutines and rethrows them so a failing launch{} fails the test. The lambda is a suspend block receiving a TestScope (this: TestScope), giving access to advanceTimeBy, runCurrent, and testScheduler.
code
kotlin · 8 lines@Test
fun timeoutLogic() = runTest {
val service = OrderService()
// production code calls delay(30_000) internally on timeout
val result = service.placeWithTimeout()
assertEquals(Result.TimedOut, result)
assertEquals(30_000, currentTime) // virtual time advanced, no real wait
}go deeper
Knows runTest runs suspend tests and that delays are skipped so tests are fast.
Explains the virtual clock, auto-advancing children to idle, and TestScope vs runBlocking's real clock.
Adds uncaught-exception aggregation, TestResult/multiplatform reason for returning it, and the scheduler being the single source of virtual time.
Frames runTest as the deterministic-time substitute for wall-clock concurrency, and discusses when NOT to use it (true I/O timing, real thread interplay).
## What problem runTest solves Testing `suspend` functions and coroutine-based code has two pain points: (1) real `delay()` calls make tests slow and flaky, and (2) work launched in background coroutines (`launch`, `async`) may not finish before the test assertion runs. `runTest` from the `kotlinx-coroutines-test` artifact solves both. ## The virtual clock `runTest` creates a `TestScope` that owns a `TestCoroutineScheduler`. The scheduler holds a **virtual clock** — a simulated notion of time. When code calls `delay(10_000)`, the coroutine does not really sleep; it registers a resume event at virtual-time +10s and yields. The scheduler then jumps the virtual clock forward to that point with **no real wall-clock wait**. This is why a test covering a 30-second timeout still runs in milliseconds. ```kotlin @Test fun fastDelay() = runTest { val start = currentTime // TestScope virtual time, starts at 0 delay(10_000) // skipped, not really waited assertEquals(10_000, currentTime) } ``` ## Auto-advancing The body lambda runs first; when it suspends or finishes, `runTest` repeatedly advances virtual time until the scheduler has no more pending tasks (everything launched in the scope is idle). So child coroutines you `launch {}` inside the scope are driven to completion automatically — you usually do not need to call `join()`. ```kotlin @Test fun childCompletes() = runTest { var done = false launch { delay(1_000); done = true } // body suspends here; runTest auto-advances // (read `done` after the launched job finishes) } ``` ## Uncaught exceptions surface If a child coroutine throws, `runTest` collects that exception and rethrows it at the end, failing the test. With plain `runBlocking` an exception in a detached `launch` could be lost or crash differently. ## Contrast with runBlocking - `runBlocking` — real dispatcher, **real clock**: `delay` truly blocks the calling thread. Fine for production glue code, bad for fast tests. - `runTest` — `TestScope` + virtual clock: delays skipped, auto-advances, surfaces child failures. Returns a `TestResult` (on JVM it just runs; the type matters for multiplatform/JS). ## Key APIs in scope Inside the lambda `this` is a `TestScope`, exposing `testScheduler`, `currentTime`, `advanceTimeBy`, `advanceUntilIdle`, and `runCurrent`. The artifact also gives you `StandardTestDispatcher` and `UnconfinedTestDispatcher`.
- Does runTest return a value, and why does the signature use `= runTest { ... }`?It returns a TestResult. The `@Test fun x() = runTest {}` form returns that result; on JVM it's effectively Unit, but on JS/native the test framework awaits the returned promise, so always return it rather than discarding it.
- Will a long delay inside a launched child still be skipped?Yes — any delay scheduled on the TestScope's scheduler is virtual, whether it's in the body or in a child launch/async, as long as that coroutine uses the test dispatcher.
Like fast-forwarding a recorded video: you still hit every frame (every delay/event) but you don't wait in real time.
saying these in an interview costs you the question
- Saying runTest actually sleeps for the delay duration
- Claiming runTest and runBlocking are interchangeable
- Thinking you must add real Thread.sleep to wait for coroutines
- Not knowing it comes from kotlinx-coroutines-test