Testing
A layered architecture is also a testable architecture. Most logic can be covered by fast JVM unit tests; only UI behavior needs the Compose test rule or an emulator.
The testing pyramid
| Test type | Runs on | Speed | Tests |
|---|---|---|---|
| Unit | JVM | fast | ViewModels, use cases, repositories, pure logic |
| Compose UI | JVM (Robolectric) or device | medium | composable behavior |
| Instrumented / E2E | emulator/device | slow | full flows, Room, navigation |
CI runs the unit (and Robolectric-backed Compose) tests; instrumented tests are run locally.
Unit-testing a ViewModel
Inject a fake repository and a test dispatcher:
class FakeArticleRepository(private val data: List<Article>) : ArticleRepository {
override suspend fun articles(): List<Article> = data
}
@OptIn(ExperimentalCoroutinesApi::class)
class ArticlesViewModelTest {
@Test
fun `load emits Success`() = runTest {
Dispatchers.setMain(StandardTestDispatcher(testScheduler))
val vm = ArticlesViewModel(FakeArticleRepository(listOf(Article(1, "t", "b"))))
vm.load()
advanceUntilIdle()
val state = vm.uiState.value
assertTrue(state is ArticlesUiState.Success)
Dispatchers.resetMain()
}
}
The example app’s ArticlesViewModelTest follows this exact pattern and runs in
CI.
Compose UI tests
class CounterScreenTest {
@get:Rule val rule = createComposeRule()
@Test
fun increments() {
rule.setContent { CounterScreen() }
rule.onNodeWithText("Increment").performClick()
rule.onNodeWithText("Count: 1").assertExists()
}
}
Run these on the JVM with Robolectric (testImplementation) or on a device
as instrumented tests. The example app’s CounterScreenTest is exactly this — a
Robolectric-backed Compose UI test that runs in CI (no emulator):
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [34])
class CounterScreenTest {
@get:Rule val composeTestRule = createComposeRule()
@Test
fun clickingIncrement_incrementsCount() {
composeTestRule.setContent { CounterScreen(viewModel = CounterViewModel()) }
composeTestRule.onNodeWithText("Increment").performClick()
composeTestRule.onNodeWithText("Count: 1").assertIsDisplayed()
}
}
It needs androidx.compose.ui:ui-test-junit4 + ui-test-manifest, Robolectric,
and testOptions { unitTests { isIncludeAndroidResources = true } }.
Testing flows
Use runTest and collect, or libraries like Turbine:
@Test
fun emits() = runTest {
val values = repository.articlesFlow().first()
assertEquals(2, values.size)
}
Exercises
-
Write a
FakeArticleRepositorythat throws, and assert the ViewModel emitsError. -
Add a Compose UI test asserting the
Resetbutton returns the count to 0.
Previous: Modularization & Build Variants · Next: Performance & Best Practices