Testing Coroutines and Code with Dependencies
Prerequisites: Testing with kotlin.test and JUnit, StateFlow, SharedFlow, and Channels
Testing code that uses coroutines, timers, and outside services needs a few extra tools.
runTest and virtual time
The kotlinx-coroutines-test library provides runTest, which runs a
coroutine test and skips delays using virtual time — a test for code
that waits ten seconds finishes instantly:
// build.gradle.kts
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2")
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class RetryTest {
private suspend fun fetchWithRetry(attempts: Int, call: suspend () -> String?): String? {
repeat(attempts) {
call()?.let { return it }
delay(5_000) // wait before retrying
}
return null
}
@Test
fun `retries until a call succeeds`() = runTest {
var calls = 0
val result = fetchWithRetry(3) { calls++; if (calls < 3) null else "ok" }
assertEquals("ok", result)
assertEquals(3, calls)
assertEquals(10_000, currentTime) // two virtual 5-second waits
}
}
currentTime shows how much virtual time passed — the test itself ran
in milliseconds.
Fakes for dependencies
Classes that depend on interfaces can be tested with simple fake implementations:
class FakeArticleRepository(
private val articles: List<Article> = emptyList(),
private val fail: Boolean = false,
) : ArticleRepository {
override suspend fun latest(): List<Article> {
if (fail) throw IOException("offline")
return articles
}
}
Fakes like this are often clearer than mocking libraries. When you do need mocks — to verify a call was made, for example — MockK is the standard Kotlin choice and supports suspend functions.
Testing a ViewModel
ViewModels launch coroutines on the main dispatcher, which doesn't exist in plain unit tests. Replace it with a test dispatcher:
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
class ArticlesViewModelTest {
private val dispatcher = StandardTestDispatcher()
@BeforeTest fun setUp() = Dispatchers.setMain(dispatcher)
@AfterTest fun tearDown() = Dispatchers.resetMain()
@Test
fun `shows articles after loading`() = runTest(dispatcher) {
val articles = listOf(Article(1, "Hello"))
val viewModel = ArticlesViewModel(FakeArticleRepository(articles))
advanceUntilIdle()
assertEquals(ArticlesUiState.Loaded(articles), viewModel.state.value)
}
}
For testing flows that emit several values over time, the Turbine library makes assertions easy to read.
What to test
Focus on logic: ViewModels, repositories, validation, and mapping. Keep UI tests (Compose's testing APIs, Espresso) for the most important user journeys — they're slower and more fragile.