What you'll learn
Automated tests are what let you change code with confidence — they catch regressions before your users do. The clean architecture you've built (ViewModels, repositories behind interfaces, stateless composables) was designed to be testable. Now let's reap that reward with two kinds of tests.
By the end you'll be able to:
- Explain why automated testing matters
- Unit-test a ViewModel with a fake repository
- Write a Compose UI test with
createComposeRule - Find nodes and assert on them
Why test
Manual testing — tap through the app after every change — is slow, boring, and easy to skip. Automated tests run in seconds, every time, and never forget an edge case. They document how your code is meant to behave and, most importantly, give you the courage to refactor: if the tests still pass, you didn't break anything. There are two levels you'll write most:
| Kind | Tests | Runs on | Speed |
|---|---|---|---|
Unit test | Logic — ViewModels, repositories, mappers | The JVM (no device) | Milliseconds |
UI test | Composables — what the user sees & taps | A device/emulator | Seconds |
Unit-testing a ViewModel
Because a ViewModel takes its dependencies via the constructor, you inject a fake repository — no database, no network — call a function, and assert the resulting state. Coroutines are made deterministic with runTest:
class CounterViewModelTest {
@Test
fun increment_increasesCount() = runTest { // runTest drives coroutines
val viewModel = CounterViewModel(FakeRepository())
viewModel.increment()
assertEquals(1, viewModel.count.value)
}
}Key idea
FakeRepository that returns canned data makes the test fast, deterministic, and isolated — it checks your logic, not the network. Every architectural choice in Phases 3–6 was, in part, to make this test easy to write.Compose UI tests
Compose has a first-class testing API. createComposeRule() lets you render a composable in isolation, then interact with it and assert on what's shown. Every UI test follows the same rhythm — set content, assert, act, assert:
class CounterScreenTest {
@get:Rule val composeRule = createComposeRule()
@Test
fun tappingButton_incrementsDisplayedCount() {
// 1) Set the content under test.
composeRule.setContent { CounterScreen() }
// 2) Assert the starting state.
composeRule.onNodeWithText("Count: 0").assertIsDisplayed()
// 3) Act — find the button and click it.
composeRule.onNodeWithText("Increment").performClick()
// 4) Assert the new state.
composeRule.onNodeWithText("Count: 1").assertIsDisplayed()
}
}Finders & assertions
UI tests work against the semantics tree — the same accessibility information screen readers use, which is why good contentDescriptions help tests too. You find a node, then act or assert on it:
| Step | Examples |
|---|---|
| Find | onNodeWithText("Increment"), onNodeWithTag("submit_button"), onNodeWithContentDescription(...) |
| Act | performClick(), performTextInput("ada@x.com"), performScrollTo() |
| Assert | assertIsDisplayed(), assertIsEnabled(), assertTextEquals(...) |
When text isn't stable or unique, add a Modifier.testTag("…") as a reliable hook for the test to find:
Button(
onClick = onSubmit,
modifier = Modifier.testTag("submit_button"), // stable hook for tests
) { Text("Log in") }
// In the test:
composeRule.onNodeWithTag("submit_button").performClick()Tip
Recap & quick check
Key takeaways
- Automated tests catch regressions fast and give you the confidence to refactor.
- Unit-test ViewModels by injecting a fake repository and asserting on the resulting state; use runTest for coroutines.
- createComposeRule() renders a composable in isolation for UI tests.
- Every UI test: set content → assert → act (click/type) → assert; work via finders and assertions.
- Use testTag as a stable finder; write many unit tests and fewer, focused UI tests of key flows.
Quick check
1. A ViewModel unit test injects a fake repository so the test is…
2. What sets up a Compose UI test?
3. The typical UI-test rhythm is…
4. Compose UI tests find nodes via…
5. When text isn't unique, the reliable way to locate a node in a test is…
Your app is tested and trustworthy. The last professional step: handling permissions and publishing to the Play Store.