Phase 6 · Professional AndroidModule 30~30 min read

Testing Compose Apps

Prove your app works with unit tests for logic and Compose UI tests for the interface.

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:

KindTestsRuns onSpeed
Unit testLogic — ViewModels, repositories, mappersThe JVM (no device)Milliseconds
UI testComposables — what the user sees & tapsA device/emulatorSeconds
Write many fast unit tests and fewer, focused UI tests.

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:

CounterViewModelTest.kt
class CounterViewModelTest {
    @Test
    fun increment_increasesCount() = runTest {   // runTest drives coroutines
        val viewModel = CounterViewModel(FakeRepository())

        viewModel.increment()

        assertEquals(1, viewModel.count.value)
    }
}

Key idea

This is the entire reason for interfaces and DI. A 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:

CounterScreenTest.kt
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:

StepExamples
FindonNodeWithText("Increment"), onNodeWithTag("submit_button"), onNodeWithContentDescription(...)
ActperformClick(), performTextInput("ada@x.com"), performScrollTo()
AssertassertIsDisplayed(), assertIsEnabled(), assertTextEquals(...)

When text isn't stable or unique, add a Modifier.testTag("…") as a reliable hook for the test to find:

TestTag.kt
Button(
    onClick = onSubmit,
    modifier = Modifier.testTag("submit_button"),   // stable hook for tests
) { Text("Log in") }

// In the test:
composeRule.onNodeWithTag("submit_button").performClick()

Tip

Aim for a healthy mix: lots of quick unit tests for logic and edge cases, plus a handful of UI tests for the critical flows (login works, adding an item shows it). Test behavior users care about, not implementation details — and write the test as you build the feature, while the requirements are fresh.

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.