Learn how to test Kotlin coroutines in Android using kotlinx-coroutines-test, runTest, test dispatchers, virtual time, coroutine scheduling, cancellation, timeouts, retries, exponential backoff, debounce, concurrency, race conditions and deterministic asynchronous tests.
Required Test Dependency
dependencies {
testImplementation(
"org.jetbrains.kotlinx:kotlinx-coroutines-test:<version>"
)
}Prefer a version catalog:
[libraries]
coroutines-test = {
module = "org.jetbrains.kotlinx:kotlinx-coroutines-test",
version.ref = "coroutines"
}dependencies {
testImplementation(libs.coroutines.test)
}1. Problems with Testing Asynchronous Code
Asynchronous code does not always finish immediately or in a predictable order.
Common problems include:
- Test completes before coroutine completes
- Real delays make tests slow
- Result arrives on another thread
- Multiple operations complete in unexpected order
- Exceptions are lost inside coroutines
- Infinite jobs never finish
- Race conditions produce inconsistent results
Dispatchers.Mainis unavailable in local JVM tests- Tests depend on real clock or network timing
Incorrect test
@Test
fun loadUser_updatesName() {
viewModel.loadUser()
assertEquals("Mahesh", viewModel.state.value.name)
}loadUser() may still be running when the assertion executes.
Incorrect workaround
Thread.sleep(2_000)This makes tests:
- Slow
- Unreliable
- Machine-dependent
- Difficult to maintain
Coroutine testing tools solve this by giving the test control over scheduling and time.
2. kotlinx-coroutines-test
kotlinx-coroutines-test is the coroutine testing library.
Important APIs include:
| API | Purpose |
|---|---|
runTest { ... } | Creates a controlled coroutine test environment with virtual time support. |
TestScope | Provides a coroutine scope for launching and managing coroutines during tests. |
TestCoroutineScheduler | Controls virtual time and the execution order of coroutine tasks. |
TestDispatcher | Dispatches coroutine work through the TestCoroutineScheduler. |
StandardTestDispatcher | Queues coroutine execution until the scheduler is advanced, enabling deterministic testing. |
UnconfinedTestDispatcher | Starts new coroutines eagerly without requiring scheduler advancement for initial execution. |
advanceUntilIdle() | Executes all pending coroutine tasks until no work remains. |
advanceTimeBy(timeMillis) | Advances virtual time by the specified duration, triggering delayed coroutines. |
runCurrent() | Executes all tasks scheduled for the current virtual time without advancing time. |
backgroundScope | Launches coroutines that are intended to outlive the main test body. |
Dispatchers.setMain(testDispatcher) | Replaces the Main dispatcher with a test dispatcher during unit tests. |
Dispatchers.resetMain() | Restores the original Main dispatcher after the test completes. |
3. runTest
runTest is the standard entry point for testing suspending code.
@Test
fun repositoryReturnsUser() = runTest {
val user = repository.getUser()
assertEquals(
User(id = 1, name = "Mahesh"),
user
)
}It provides:
- A test coroutine scope
- Virtual-time support
- Structured exception handling
- Automatic skipping of controlled delays
- Cleanup checks
- Timeout protection for tests that do not finish
Prefer:
@Test
fun loadData_returnsResult() = runTest {
// Test
}Avoid older APIs such as:
runBlockingTest { }4. Test Coroutine Scope
runTest uses a TestScope.
A TestScope contains:
- Coroutine context
- Test dispatcher
- Test scheduler
- Virtual-time controls
- Background scope
@Test
fun testScopeExample() = runTest {
var result = 0
launch {
delay(1_000)
result = 10
}
advanceUntilIdle()
assertEquals(10, result)
}The coroutine launched inside runTest is its child. The test can track its work and failures.
Standalone TestScope
val scheduler = TestCoroutineScheduler()
val scope = TestScope(
StandardTestDispatcher(scheduler)
)Usually, using the scope supplied by runTest is simpler.
5. Test Coroutine Scheduler
TestCoroutineScheduler controls:
- Task execution
- Virtual clock
- Delayed coroutine resumption
- Coordination between test dispatchers
flowchart TD
TS["TestCoroutineScheduler"]
D1["Main TestDispatcher"]
D2["IO TestDispatcher"]
C1["Coroutine A"]
C2["Coroutine B"]
TS --> D1
TS --> D2
D1 --> C1
D2 --> C2All test dispatchers in one test should normally share the same scheduler.
@Test
fun sharedSchedulerExample() = runTest {
val mainDispatcher =
StandardTestDispatcher(testScheduler)
val ioDispatcher =
StandardTestDispatcher(testScheduler)
val viewModel = UserViewModel(
repository = repository,
mainDispatcher = mainDispatcher,
ioDispatcher = ioDispatcher
)
}If dispatchers use different schedulers, advancing one scheduler may not run work assigned to the other.
6. Virtual Time
Virtual time lets a test simulate time without waiting in real time.
@Test
fun delayedMessage_isDeliveredAfterOneSecond() = runTest {
var message: String? = null
launch {
delay(1_000)
message = "Completed"
}
assertNull(message)
advanceTimeBy(1_000)
runCurrent()
assertEquals("Completed", message)
}Although the application delay is one second, the test executes almost immediately.
Virtual time works only for delays scheduled on compatible test dispatchers.
7. TestDispatcher
TestDispatcher is a coroutine dispatcher designed for testing.
Its work is controlled by a TestCoroutineScheduler.
The main implementations are:
StandardTestDispatcherUnconfinedTestDispatcher
Production:
Dispatchers.IO
Dispatchers.Main
Dispatchers.DefaultTest:
StandardTestDispatcher(testScheduler)Inject dispatchers into production classes so tests can replace real dispatchers.
class LoadUserUseCase(
private val repository: UserRepository,
private val ioDispatcher: CoroutineDispatcher
) {
suspend operator fun invoke(): User =
withContext(ioDispatcher) {
repository.getUser()
}
}8. StandardTestDispatcher
StandardTestDispatcher queues newly launched coroutine work. The test decides when to execute it.
@Test
fun standardDispatcher_queuesCoroutine() = runTest {
val dispatcher =
StandardTestDispatcher(testScheduler)
var completed = false
launch(dispatcher) {
completed = true
}
assertFalse(completed)
runCurrent()
assertTrue(completed)
}Best for:
- Testing exact state transitions
- Testing delayed work
- Testing concurrency
- Controlling execution order
- ViewModel testing
- Retry and debounce testing
It provides strong control and is generally the safer default.
9. UnconfinedTestDispatcher
UnconfinedTestDispatcher eagerly starts new coroutines.
@Test
fun unconfinedDispatcher_startsCoroutineImmediately() = runTest {
val dispatcher =
UnconfinedTestDispatcher(testScheduler)
var started = false
launch(dispatcher) {
started = true
delay(1_000)
}
assertTrue(started)
}It does not guarantee that the entire coroutine completes immediately after suspension.
Best for:
- Simple tests
- Immediately starting collectors
- Reducing explicit scheduler advancement
- Tests where execution ordering is unimportant
Avoid relying on it for production-like thread ordering.
10. Choosing the Correct Test Dispatcher
| Requirement | Recommended dispatcher |
|---|---|
| Precise scheduling control | StandardTestDispatcher |
| State-transition verification | StandardTestDispatcher |
| Delay, retry or debounce | StandardTestDispatcher |
| Concurrent operation ordering | StandardTestDispatcher |
| Start a Flow collector immediately | UnconfinedTestDispatcher |
| Simple suspend function | runTest default may be sufficient |
| Complex ViewModel logic | StandardTestDispatcher |
Recommended beginner rule:
Start with StandardTestDispatcher. Use UnconfinedTestDispatcher only when eager execution makes the test clearer.
11. advanceUntilIdle()
advanceUntilIdle() executes scheduled tasks and advances virtual time until no executable tasks remain.
@Test
fun loadProfile_updatesState() = runTest {
viewModel.loadProfile()
advanceUntilIdle()
assertEquals(
ProfileState.Success(expectedProfile),
viewModel.state.value
)
}Use it when:
- Only the final result matters
- You want all finite scheduled work to finish
- The implementation contains delays
- ViewModel work was launched in
viewModelScope
Do not use it carelessly with uncontrolled infinite work.
12. advanceTimeBy()
advanceTimeBy(duration) advances the scheduler’s virtual clock by a specific duration.
@Test
fun message_isNotShownBeforeTimeout() = runTest {
viewModel.startTimer()
advanceTimeBy(999)
runCurrent()
assertFalse(viewModel.state.value.showMessage)
advanceTimeBy(1)
runCurrent()
assertTrue(viewModel.state.value.showMessage)
}It is useful for testing:
- Timeout boundaries
- Retry delays
- Debounce
- Countdown logic
- Cache expiry
- Exponential backoff
Calling runCurrent() after advancing time makes boundary behaviour explicit.
13. runCurrent()
runCurrent() executes tasks scheduled at the current virtual time without advancing to future delays.
@Test
fun runCurrent_executesImmediateWorkOnly() = runTest {
val events = mutableListOf<String>()
launch {
events += "Started"
delay(1_000)
events += "Completed"
}
runCurrent()
assertEquals(
listOf("Started"),
events
)
}Use it to:
- Start queued coroutines
- Inspect intermediate state
- Execute tasks at the current time
- Avoid completing future delayed work
Time-Control Comparison
| Function | Runs Current Work | Advances Virtual Time | Runs All Finite Work |
|---|---|---|---|
runCurrent() | ✅ Yes | ❌ No | ❌ No |
advanceTimeBy(duration) | ✅ Yes (within the specified interval) | ✅ Yes | ❌ No |
advanceUntilIdle() | ✅ Yes | ✅ Yes | ✅ Yes (until no pending tasks remain) |
14. Testing Delayed Operations
Production code
class SplashViewModel(
private val dispatcher: CoroutineDispatcher
) : ViewModel() {
private val _navigate =
MutableStateFlow(false)
val navigate = _navigate.asStateFlow()
fun start() {
viewModelScope.launch(dispatcher) {
delay(2_000)
_navigate.value = true
}
}
}Test
@Test
fun start_afterTwoSeconds_enablesNavigation() = runTest {
val dispatcher =
StandardTestDispatcher(testScheduler)
val viewModel = SplashViewModel(dispatcher)
viewModel.start()
advanceTimeBy(1_999)
runCurrent()
assertFalse(viewModel.navigate.value)
advanceTimeBy(1)
runCurrent()
assertTrue(viewModel.navigate.value)
}This verifies both sides of the time boundary.
15. Testing Concurrent Operations
Concurrent operations may complete in a different order from which they started.
@Test
fun concurrentRequests_bothResultsAreCollected() = runTest {
val results = mutableListOf<String>()
val first = async {
delay(1_000)
"Profile"
}
val second = async {
delay(500)
"Transactions"
}
results += awaitAll(first, second)
assertEquals(
listOf("Profile", "Transactions"),
results
)
}awaitAll() returns values in the order of its deferred arguments, not necessarily completion order.
Test concurrency for:
- Parallel API requests
- Search query replacement
- Cache and network races
- Multiple database operations
- Cancellation of previous work
- Shared-state updates
Focus on the required outcome rather than assuming a thread-execution order.
16. Testing Coroutine Results
For a suspending function, call it inside `runTest` and assert its result.
@Test
fun loadBalance_repositorySucceeds_returnsBalance() = runTest {
coEvery {
repository.getBalance()
} returns 5_000L
val result = useCase()
assertEquals(5_000L, result)
}For a launched coroutine, advance the scheduler:
@Test
fun refresh_updatesState() = runTest {
viewModel.refresh()
advanceUntilIdle()
assertEquals(
UiState.Success(expectedData),
viewModel.state.value
)
}17. Testing Coroutine Exceptions
Direct suspend-function exception
@Test
fun loadUser_repositoryFails_throwsIOException() = runTest {
coEvery {
repository.getUser()
} throws IOException("No network")
assertThrows(IOException::class.java) {
runTest {
useCase()
}
}
}A simpler coroutine-friendly pattern is:
@Test
fun loadUser_repositoryFails_throwsIOException() = runTest {
coEvery {
repository.getUser()
} throws IOException("No network")
val exception = runCatching {
useCase()
}.exceptionOrNull()
assertTrue(exception is IOException)
assertEquals("No network", exception?.message)
}ViewModel error mapping
@Test
fun repositoryThrowsIOException_stateBecomesNetworkError() = runTest {
coEvery {
repository.getUser()
} throws IOException()
viewModel.loadUser()
advanceUntilIdle()
assertEquals(
UiState.Error(UiError.Network),
viewModel.state.value
)
}Unhandled exceptions in child coroutines normally cause runTest to fail, which helps reveal hidden coroutine errors.
18. Testing Cancellation
Production operation
suspend fun downloadFile(): String {
delay(10_000)
return "Downloaded"
}Test
@Test
fun cancelledDownload_doesNotComplete() = runTest {
var completed = false
val job = launch {
downloadFile()
completed = true
}
runCurrent()
job.cancelAndJoin()
advanceUntilIdle()
assertTrue(job.isCancelled)
assertFalse(completed)
}Test cancellation when:
- User leaves a screen
- Search query changes
- ViewModel clears
- Timeout occurs
- New operation replaces old operation
- Parent coroutine fails
Production code must cooperate with cancellation. Blocking operations that do not suspend may ignore cancellation.
19. Testing Timeouts
Production code
suspend fun loadWithTimeout(): Result<Data> {
return try {
Result.success(
withTimeout(3_000) {
repository.load()
}
)
} catch (exception: TimeoutCancellationException) {
Result.failure(exception)
}
}Test
@Test
fun requestExceedsThreeSeconds_returnsFailure() = runTest {
coEvery {
repository.load()
} coAnswers {
delay(5_000)
expectedData
}
val result = loadWithTimeout()
assertTrue(result.isFailure)
assertTrue(
result.exceptionOrNull()
is TimeoutCancellationException
)
}Virtual time allows the timeout to happen without waiting three real seconds.
20. Testing Retry Logic
Production function
suspend fun <T> retry(
times: Int,
delayMillis: Long,
operation: suspend () -> T
): T {
var lastError: Throwable? = null
repeat(times) { attempt ->
try {
return operation()
} catch (error: IOException) {
lastError = error
if (attempt < times - 1) {
delay(delayMillis)
}
}
}
throw lastError ?: IllegalStateException()
}Test: succeeds on third attempt
@Test
fun operationFailsTwice_thenSucceeds() = runTest {
var attempts = 0
val result = retry(
times = 3,
delayMillis = 1_000
) {
attempts++
if (attempts < 3) {
throw IOException()
}
"Success"
}
assertEquals("Success", result)
assertEquals(3, attempts)
assertEquals(2_000, testScheduler.currentTime)
}Also test:
- Success on first attempt
- Failure after maximum attempts
- Only retryable exceptions are retried
- Cancellation stops retrying
- No delay after final failure
21. Testing Exponential Backoff
Exponential backoff increases the delay after each failure.
Example:
Attempt 1 fails → wait 1 second
Attempt 2 fails → wait 2 seconds
Attempt 3 fails → wait 4 secondsA common formula is:
[
Delay_n = InitialDelay \times 2^n
]Test
@Test
fun retry_usesExponentialBackoff() = runTest {
var attempts = 0
val result = retryWithBackoff(
maxAttempts = 4,
initialDelay = 1_000
) {
attempts++
if (attempts < 4) {
throw IOException()
}
"Success"
}
assertEquals("Success", result)
assertEquals(4, attempts)
// 1,000 + 2,000 + 4,000
assertEquals(
7_000,
testScheduler.currentTime
)
}Also consider:
- Maximum delay cap
- Jitter
- Cancellation
- Retryable vs non-retryable errors
22. Testing Debounce
Debounce waits for a quiet period and uses the most recent value.
Common use case: search-as-you-type.
flowchart LR
A["A"]
AN["An"]
AND["And"]
WAIT["Wait 300 ms"]
SEARCH["Search: And"]
A --> AN --> AND --> WAIT --> SEARCHExample test
@Test
fun rapidQueries_onlyLatestQueryIsSearched() = runTest {
val queries = MutableSharedFlow<String>()
val results = mutableListOf<String>()
val job = backgroundScope.launch(
UnconfinedTestDispatcher(testScheduler)
) {
queries
.debounce(300)
.collect { results += it }
}
queries.emit("A")
advanceTimeBy(100)
queries.emit("An")
advanceTimeBy(100)
queries.emit("Android")
advanceTimeBy(299)
runCurrent()
assertTrue(results.isEmpty())
advanceTimeBy(1)
runCurrent()
assertEquals(
listOf("Android"),
results
)
job.cancel()
}Debounce tests should check:
- No early emission
- Latest value emitted
- Earlier values discarded
- Exact time boundary
- Cancellation
23. Testing Throttling
Throttling limits how often an action can occur.
Example:
Accept at most one click every 1,000 milliseconds.
Click at 0 ms → Accepted
Click at 200 ms → Ignored
Click at 700 ms → Ignored
Click at 1,000 ms → AcceptedTest:
@Test
fun rapidClicks_onlyOneActionPerWindow() = runTest {
viewModel.onPayClicked()
runCurrent()
viewModel.onPayClicked()
advanceTimeBy(999)
runCurrent()
coVerify(exactly = 1) {
repository.submitPayment()
}
advanceTimeBy(1)
viewModel.onPayClicked()
runCurrent()
coVerify(exactly = 2) {
repository.submitPayment()
}
}Inject a testable clock or implement throttling using coroutine time so the test does not use the real clock.
24. Testing Race Conditions
A race condition occurs when the result depends on unpredictable operation timing.
Example
Two search requests run:
- Request "Android" starts first
- Request "Android Testing" starts second
- Second request finishes first
- First request finishes later and overwrites the latest result
Correct behaviour should show the latest query’s result.
@Test
fun oldSearchFinishesLast_doesNotReplaceLatestResult() = runTest {
repository.delayFor["Android"] = 1_000
repository.delayFor["Kotlin"] = 100
viewModel.search("Android")
viewModel.search("Kotlin")
advanceUntilIdle()
assertEquals(
SearchState.Success(
query = "Kotlin",
results = kotlinResults
),
viewModel.state.value
)
}Strategies include:
flatMapLatest- Cancelling previous jobs
- Mutex
- Atomic operations
- Immutable state updates
- Request identifiers
- Ignoring outdated responses
A scheduler can reproduce selected interleavings, but it cannot prove that every possible production race is absent. Also use stress tests and synchronization-safe design where appropriate.
25. Testing Long-Running Coroutines
Long-running coroutines include:
- Flow collectors
- Polling loops
- WebSocket listeners
- Location updates
- Repeated synchronization
- Countdown timers
Problem:
launch {
while (true) {
delay(1_000)
refresh()
}
}If launched as normal test work, the test may never become idle.Test a limited number of iterations or run the collector in backgroundScope.
@Test
fun polling_refreshesEverySecond() = runTest {
viewModel.startPolling()
advanceTimeBy(3_000)
runCurrent()
assertEquals(3, repository.refreshCount)
viewModel.stopPolling()
}Ensure long-running jobs have an explicit cancellation mechanism.
26. Using backgroundScope
backgroundScope is designed for coroutines that should run while the test continues but are not expected to complete naturally.
@Test
fun sharedFlow_emitsUpdates() = runTest {
val values = mutableListOf<Int>()
backgroundScope.launch(
UnconfinedTestDispatcher(testScheduler)
) {
repository.updates.collect {
values += it
}
}
repository.emit(1)
repository.emit(2)
assertEquals(listOf(1, 2), values)
}Background jobs are cancelled when runTest finishes.
Use it for:
- Infinite Flow collection
- Event observation
- Polling
- Long-lived listeners
- Hot streams
27. Replacing Dispatchers.Main
Local JVM tests do not provide Android’s real Main dispatcher.
ViewModels commonly use viewModelScope, which uses Dispatchers.Main.
Replace it during tests:
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}Example:
private val testDispatcher =
StandardTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}Always reset Main to prevent one test from affecting another.
28. Main Dispatcher Test Rule
A reusable JUnit 4 rule avoids repeating setup and cleanup.
class MainDispatcherRule(
val testDispatcher: TestDispatcher =
StandardTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}Usage
class UserViewModelTest {
@get:Rule
val mainDispatcherRule =
MainDispatcherRule()
@Test
fun loadUser_updatesState() = runTest {
val viewModel = UserViewModel(
repository = fakeRepository,
ioDispatcher =
mainDispatcherRule.testDispatcher
)
viewModel.loadUser()
advanceUntilIdle()
assertEquals(
UserState.Success(expectedUser),
viewModel.state.value
)
}
}The Main test dispatcher’s scheduler can become the scheduler used by subsequently created test dispatchers. Still, keep scheduler sharing explicit in complex tests.
29. Avoiding Real Delays
Do not write:
Thread.sleep(3_000)Avoid tests depending on:
delay(3_000) // On a real dispatcherUse:
advanceTimeBy(3_000)
runCurrent()Real delays cause
- Slow test suites
- Flaky failures
- Dependence on CPU speed
- Longer CI feedback
- Unnecessary waiting
- Timing assumptions
Also avoid production code secretly switching to a real dispatcher that the test cannot control.
30. Creating Deterministic Asynchronous Tests
A deterministic test produces the same result every time.
Deterministic design
Inject and control:
- Coroutine dispatchers
- Clock
- Random-number generator
- UUID generator
- Network responses
- Database contents
- Retry policy
- Test data
- Feature flags
flowchart TD
INPUT["Fixed input"]
SCHEDULER["Controlled scheduler"]
DEP["Fake dependencies"]
TIME["Virtual time"]
RESULT["Repeatable result"]
INPUT --> RESULT
SCHEDULER --> RESULT
DEP --> RESULT
TIME --> RESULTChecklist
- Use
runTest. - Use one shared scheduler.
- Replace
Dispatchers.Main. - Inject other dispatchers.
- Avoid real network and database calls.
- Avoid real time and random values.
- Advance virtual time explicitly.
- Start event collectors before emitting.
- Cancel infinite work.
- Assert meaningful state and results.
- Keep tests independent.
31. Coroutine-Testing Common Mistakes
1. Using runBlocking instead of runTest2. Using deprecated runBlockingTest3. Adding Thread.sleep()4. Using real dispatchers
5. Hardcoding Dispatchers.IO6. Forgetting to replace Dispatchers.Main7. Forgetting Dispatchers.resetMain()8. Creating test dispatchers with different schedulers
9. Asserting before queued work runs
10. Calling advanceUntilIdle() with uncontrolled infinite work
11. Using UnconfinedTestDispatcher everywhere
12. Assuming eager start means immediate completion
13. Ignoring cancellation
14. Catching CancellationException as a normal error
15. Collecting an infinite Flow in the main test scope
16. Starting a SharedFlow collector too late
17. Testing only final state when transition order matters
18. Depending on exact coroutine execution order unnecessarily
19. Using uncontrolled random jitter
20. Retrying non-retryable exceptions
21. Forgetting to test maximum retry failure
22. Hiding production defects with test retries
23. Testing implementation timing instead of business behaviour
24. Leaving launched jobs active
25. Assuming a scheduler test proves all race conditions are impossible
Complete Use Case: Search ViewModel
Behaviour
- User types a search query.
- Search waits for 300 milliseconds.
- Only the latest query is executed.
- Previous search is cancelled.
- Loading and result states are emitted.
- Network errors become UI errors.
flowchart TD
Q["Query entered"]
D["Debounce 300 ms"]
C["Cancel old search"]
L["Loading"]
R["Repository search"]
S["Success or error"]
Q --> D
D --> C
C --> L
L --> R
R --> SRecommended tests
| Scenario | Expected result |
|---|---|
| Query entered | No immediate search |
| 299 ms passes | No search |
| 300 ms passes | Search begins |
| Multiple queries | Only latest query searched |
| Repository succeeds | Success state |
| Repository fails | Error state |
| New query arrives | Previous search cancelled |
| ViewModel cleared | Search coroutine cancelled |
Interview Questions and Answers
1. Why is asynchronous code difficult to test?
Because execution timing, ordering, cancellation and thread scheduling may be unpredictable.
2. What is kotlinx-coroutines-test?
It is a library that provides coroutine test scopes, dispatchers, schedulers and virtual-time control.
3. What is runTest?
It runs coroutine tests in a controlled TestScope and supports virtual time, exception reporting and structured cleanup.
4. Why use runTest instead of runBlocking?
runTest provides virtual time and test scheduling and is specifically designed for coroutine tests.
5. What is a TestScope?
It is a coroutine scope for tests containing a test scheduler and virtual-time utilities.
6. What is TestCoroutineScheduler?
It controls task execution and virtual time for one or more test dispatchers.
7. Why should test dispatchers share one scheduler?
So one scheduler operation can control all relevant coroutine work consistently.
8. What is virtual time?
It is simulated time that allows delayed coroutine work to be tested without real waiting.
9. What is a TestDispatcher?
It is a coroutine dispatcher whose execution is controlled by a TestCoroutineScheduler.
10. What is StandardTestDispatcher?
It queues coroutine work and lets the test explicitly decide when that work executes.
11. What is UnconfinedTestDispatcher?
It starts newly launched coroutines eagerly while still using the test scheduler for suspensions and delays.
12. Standard vs Unconfined TestDispatcher?
Use Standard for precise scheduling and state-transition control. Use Unconfined when eager coroutine start simplifies collection or a basic test.
13. Which dispatcher should be the default choice?
StandardTestDispatcher is generally the safer default for controlled tests.
14. What does runCurrent() do?
It runs tasks scheduled for the current virtual time without moving time forward.
15. What does advanceTimeBy() do?
It advances virtual time by a specified duration and processes eligible scheduled work.
16. What does advanceUntilIdle() do?
It runs scheduled work and advances time until no executable finite tasks remain.
17. How do you test a delayed operation?
Use a test dispatcher and advance virtual time instead of waiting in real time.
18. How do you test a suspending function result?
Call it inside runTest and assert the returned value.
19. How do you test coroutine exceptions?
Configure the dependency to fail and assert the thrown exception or resulting error state.
20. How do unhandled child-coroutine exceptions affect runTest?
They normally fail the test, preventing hidden coroutine failures.
21. How do you test cancellation?
Launch the operation, cancel its Job, join it and verify that post-cancellation work did not occur.
22. Why must coroutine code cooperate with cancellation?
Cancellation is cooperative. Long blocking or CPU work may not observe cancellation without suspension or explicit checks.
23. How do you test a timeout?
Make the operation delay beyond withTimeout and let virtual time trigger the timeout immediately.
24. How do you test retry logic?
Count attempts, control failures and verify success, failure limit and accumulated virtual delay.
25. What is exponential backoff?
It increases the delay between retry attempts, commonly by multiplying the previous delay.
26. What additional concern exists with production backoff?
Production backoff may use jitter and a maximum delay cap, which should be injected or controlled in tests.
27. What is debounce?
It waits for a quiet period and processes only the latest value.
28. How do you test debounce?
Emit values, advance virtual time around the debounce boundary and verify that only the latest value is processed.
29. What is throttling?
It limits how often an action is accepted during a time window.
30. Debounce vs throttle?
Debounce waits for inactivity and emits the latest value. Throttle permits a limited number of actions within a time interval.
31. What is a race condition?
It is a defect where the result depends on unpredictable operation timing or execution order.
32. How can search race conditions be prevented?
Cancel the previous job, use flatMapLatest, or reject responses that do not match the current request.
33. Can one coroutine test prove that all races are absent?
No. It can reproduce specific orderings, but concurrency-safe design and additional stress testing may still be required.
34. What is backgroundScope?
It is a TestScope-owned scope for long-running jobs that should remain active during the test and be cancelled afterward.
35. When should backgroundScope be used?
For infinite Flow collectors, listeners, polling and other jobs not expected to complete naturally.
36. Why replace Dispatchers.Main?
Local JVM tests do not have Android’s real main dispatcher, while viewModelScope depends on it.
37. How is Dispatchers.Main replaced?
Call Dispatchers.setMain(testDispatcher) before the test and Dispatchers.resetMain() afterward.
38. What is a Main dispatcher test rule?
A reusable JUnit rule that replaces and restores Dispatchers.Main automatically.
39. Why should Thread.sleep() be avoided?
It causes slow, timing-dependent and flaky tests.
40. What makes a coroutine test deterministic?
Controlled dependencies, a shared scheduler, virtual time, injected dispatchers and fixed input data.
Quick Revision
| Concept / API | Purpose |
|---|---|
runTest { ... } | Creates a controlled coroutine test scope with virtual time support. |
TestCoroutineScheduler | Controls coroutine execution order and manages virtual time. |
StandardTestDispatcher | Queues coroutine work for deterministic execution; ideal for precise testing. |
UnconfinedTestDispatcher | Starts new coroutines eagerly, simplifying tests that don't require strict scheduling. |
runCurrent() | Executes all coroutine work scheduled at the current virtual time. |
advanceTimeBy(duration) | Advances virtual time by the specified duration and runs eligible delayed tasks. |
advanceUntilIdle() | Executes all pending finite coroutine work until no tasks remain. |
backgroundScope | Launches long-running collectors or listeners that should outlive the main test body. |
Dispatchers.setMain(testDispatcher) | Replaces the Main dispatcher with a test dispatcher in JVM unit tests. |
Avoid Thread.sleep() | Use virtual time (advanceTimeBy() or advanceUntilIdle()) instead of blocking the thread. |
| Avoid Real Dispatchers | Inject TestDispatcher instead of using Dispatchers.IO, Default, or Main. |
| Avoid Different Test Schedulers | Share a single TestCoroutineScheduler across all test dispatchers for deterministic execution. |
| Avoid Uncontrolled Time | Control delays and timers with virtual time rather than waiting for real time. |
Avoid Late SharedFlow Collection | Start collecting before emitting events to prevent missed emissions. |
| Avoid Infinite Jobs in Main Test Scope | Launch long-running jobs in backgroundScope so they don't block test completion. |