$npx -y skills add rcosteira79/android-skills --skill android-testingUse when writing, fixing, or refactoring Android/KMP code in Kotlin — a test-first (RED-GREEN-REFACTOR) foundation plus the Android test traps: Compose-test dispatching (StandardTestDispatcher default, the two-schedulers trap), semantics-first selectors, choosing the smallest tes
| 1 | # Android Testing |
| 2 | |
| 3 | Android-specific testing on a **test-first** foundation. This reference focuses on the test-first discipline plus the Compose-test traps that are easy to get wrong, not the basics of the three tiers, fakes over mocks, `runTest`, or Given-When-Then naming. |
| 4 | |
| 5 | ## Test-first (the foundation) |
| 6 | |
| 7 | - **No production code without a failing test first.** Write the test, watch it fail (RED), write the minimal code to pass (GREEN), then refactor. A behaviour you never watched fail isn't proven. |
| 8 | - **A bug isn't fixed until a test that was red *because of the bug* is green.** |
| 9 | |
| 10 | This layers on top of any dedicated TDD discipline skill (`superpowers:test-driven-development`, `ace:test-driven-development`) but requires none. For *bootstrapping* the test stack from scratch (test DI, JUnit/Robolectric/Roborazzi/Paparazzi selection, the instrumented runner, Compose Preview Screenshot Testing, UI Automator, Jacoco), see Google's official [`testing-setup`](https://github.com/android/skills/tree/main/testing/testing-setup) skill (`android skills add testing-setup`). |
| 11 | |
| 12 | ## Compose tests now default to `StandardTestDispatcher` |
| 13 | |
| 14 | `createComposeRule()` / `runComposeUiTest {}` default to `StandardTestDispatcher` (matching `kotlinx.coroutines.test.runTest`) — **there is no separate "v2" package.** It's gated by `androidx.compose.ui.test.ComposeUiTestFlags.isStandardTestDispatcherSupportEnabled` (defaults to `true`), so the regular `androidx.compose.ui.test.junit4.createComposeRule` already uses it. To pin a scheduler, pass it through: `createComposeRule(effectContext = StandardTestDispatcher())`. Under this default, a `LaunchedEffect` that previously ran eagerly (the old `UnconfinedTestDispatcher` behaviour) may need an explicit `mainClock.advanceTimeBy(0)` / `runCurrent()` to drain queued work; set the flag `false` only to temporarily restore the legacy behaviour. |
| 15 | |
| 16 | **Two-schedulers trap** (the one coroutine-test gotcha worth stating): a `MainDispatcherRule`'s `TestDispatcher` and the dispatcher `runTest { }` creates have **separate** `TestCoroutineScheduler`s. Pass `mainRule.dispatcher` into `runTest(mainRule.dispatcher)` so `Dispatchers.Main` and the test body share one — otherwise `advanceUntilIdle()` flushes only one and assertions race the ViewModel. |
| 17 | |
| 18 | ## Semantics first, `testTag` as fallback |
| 19 | |
| 20 | Prefer user-visible semantics over `testTag` — real users and screen readers see semantics; `testTag` is invisible to everyone except tests. Selector priority: (1) `onNodeWithText`; (2) `onNodeWithContentDescription`; (3) role/state matchers (`hasClickAction()`, `isSelected()`, `isFocused()`, `isEnabled()`); (4) `onNodeWithTag` **only** when there's no stable user-visible text or it's duplicated/ambiguous (lists of identical rows, per-locale copy, multiple instances). A text assertion survives refactors and exercises accessibility; a `testTag` assertion breaks the moment the tag changes and misses the user-facing regression. |
| 21 | |
| 22 | *(Counterview worth knowing: [skydoves/android-testing-skills](https://github.com/skydoves/android-testing-skills) argues tag-first for i18n robustness, backed by androidx/material3's own 1825 : 424 : 46 `testTag` : `onNodeWithText` : `onNodeWithContentDescription` ratio. Defensible if you have separate accessibility coverage; this skill defaults to semantics-first because it catches a class of bugs `testTag` never can.)* |
| 23 | |
| 24 | ## Callbacks as test surfaces |
| 25 | |
| 26 | A composable's contract is "render state, emit callbacks" — test exactly that; don't route the assertion through a ViewModel mock. |
| 27 | |
| 28 | ```kotlin |
| 29 | @Test fun `tapping article row invokes onArticleClick with id`() { |
| 30 | var clickedId: String? = null |
| 31 | composeTestRule.setContent { ArticleRow(Article(id = "42", title = "Hello"), onArticleClick = { clickedId = it }) } |
| 32 | composeTestRule.onNodeWithText("Hello").performClick() |
| 33 | assertEquals("42", clickedId) |
| 34 | } |
| 35 | ``` |
| 36 | |
| 37 | ## Choosing the test shape — the smallest contract that proves the behaviour |
| 38 | |
| 39 | | Proving | Shape | |
| 40 | |---|---| |
| 41 | | Text rendered, conditional content, loading/error branches, callback wiring | Plain UI Compose test (state + callbacks, no graph) | |
| 42 | | Focus navigation, keyboard, TV/D-pad | Compose test with `performKeyInput` + `assertIsFocused()` (see `compose/references/focus-navigation.md`) | |
| 43 | | Visual contract semantics can't prove — spacing, themed colour, typography, elevation, gradients, skeletons | Screenshot test, one per meaningful state | |
| 44 | | State holder updates UI correctly | State-holder unit test + ONE wiring smoke test | |
| 45 | | Lifecycle, navigation, or DI integration itself | Integration test (`createAnd |