On this page
A refactor changes padding in a shared component, and twenty screens shift by 8dp. A colour token is renamed, and the error state becomes unreadable in dark mode. Unit tests won't catch either. UI tests catch broken behaviour, and screenshot tests catch broken visuals.
Where UI and screenshot tests fit
Make screens testable first
Split each screen into a Route (connects to the ViewModel) and a stateless Screen (renders a UiState and emits events). Tests and previews then drive the Screen directly with any state. See State Hoisting.
Compose UI tests
dependencies {
androidTestImplementation(platform("androidx.compose:compose-bom:<latest>"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}class LoginScreenTest {
@get:Rule val compose = createComposeRule()
@Test
fun signInDisabledUntilEmailValid_thenEmitsEvent() {
var signInClicked = false
var state by mutableStateOf(LoginUiState())
compose.setContent {
AppTheme {
LoginContent(
state = state,
onEmailChange = { state = state.copy(email = it, canSubmit = it.contains("@")) },
onSignIn = { signInClicked = true },
)
}
}
compose.onNodeWithText("Sign in").assertIsNotEnabled()
compose.onNodeWithText("Email").performTextInput("dev@example.com")
compose.onNodeWithText("Sign in").assertIsEnabled().performClick()
assertTrue(signInClicked)
}
}- Finders (
onNodeWithText,onNodeWithContentDescription,onNodeWithTag) query the semantics tree, the same information accessibility services use. Good semantics make tests easier too. - Use
Modifier.testTag("โฆ")only when no user-visible text or description identifies a node. - Compose tests synchronise with the UI automatically, waiting for recomposition and animations to idle. For async work, use
waitUntil { }.
More patterns: UI Testing in Jetpack Compose.
Screenshot testing
A screenshot test renders a composable, saves a reference image, and on later runs compares the new rendering against it, failing on visual differences.
Your options
| Tool | How it renders | Notes |
|---|---|---|
| Compose Preview Screenshot Testing (Google) | Turns @Preview functions into screenshot tests, run on the JVM | Alpha at the time of writing; setup is moving to AGP test suites. Check the current docs. |
| Roborazzi | JVM via Robolectric | Popular open-source option; integrates with Robolectric tests |
| Paparazzi | JVM using layoutlib | Open source; no device needed |
| Device/emulator-based | On real rendering stack | Most realistic, slower and noisier in CI |
Previews as screenshot tests
With Google's tool, previews placed in the screenshot test source set become tests. A multi-preview annotation makes each component render in several configurations:
@Preview(name = "Light", showBackground = true)
@Preview(name = "Dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(name = "Large font", showBackground = true, fontScale = 1.5f)
annotation class ThemePreviews@ThemePreviews
@Composable
fun OrderCardDelivered() = AppTheme { OrderCard(sampleOrder(status = Delivered)) }
@ThemePreviews
@Composable
fun OrderCardFailedPayment() = AppTheme { OrderCard(sampleOrder(status = PaymentFailed)) }You then run a Gradle task to update reference images and another to validate against them. The exact task names depend on the plugin version and whether you use the newer test-suite setup, so follow the current documentation.
Running in CI
- Commit reference images to the repository (or Git LFS).
- Run validation on every pull request, and publish the diff report as a CI artifact.
- When a change is intentional, regenerate references in the same PR so reviewers see the visual diff.
- Pin fonts, locale and rendering settings so images are deterministic.
What to screenshot
Design-system components
Buttons, cards and inputs in every state (enabled, disabled, error, loading), in light and dark, at large font scale.
Key screens
Empty, loading, error and populated states of high-traffic screens.
Localisation
Long-text languages and RTL layouts, where truncation and alignment bugs hide.
Common mistakes
- Screenshotting screens that load live data or show the current time. Inject fixed data and clocks.
- Hundreds of near-duplicate screenshots that nobody reviews.
- Using test tags everywhere instead of improving semantics.
- Blindly accepting regenerated references without looking at diffs.
Frequently asked questions
Do Compose UI tests need a device?
Instrumented tests run on a device or emulator. Compose UI tests can also run on the JVM with Robolectric for faster feedback.
Are screenshot tests flaky?
JVM-rendered screenshots are generally stable when inputs are deterministic. Device-based screenshots can vary with GPU and OS differences, so use tolerances.
Should I replace UI tests with screenshot tests?
No. They complement each other: screenshots verify appearance, and UI tests verify behaviour and interaction.
Conclusion
Structure screens as stateless, previewable composables, then use semantics-based UI tests for behaviour and screenshot tests for appearance across states, themes and font sizes. Run both in CI, and review visual diffs like code.
Next step: make those screens work beautifully on every form factor with Adaptive UI for Tablets and Foldables.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.