๐ŸŽจ Jetpack Compose

Compose UI Testing and Screenshot Testing

Functional UI tests check that a screen behaves correctly. Screenshot tests check that it still looks correct. Compose makes both practical. This guide covers semantics-based UI tests and screenshot testing, and shows how to structure screens so tests stay simple.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. Where UI and screenshot tests fit
  2. Make screens testable first
  3. Compose UI tests
  4. Screenshot testing
  5. Running in CI
  6. What to screenshot
  7. Common mistakes
  8. Frequently asked questions
  9. Conclusion

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

Android testing pyramid with screenshot tests Many fast unit tests at the base for ViewModels and logic; screenshot tests of components and screens in several states; Compose UI tests for interactions; a few end-to-end tests at the top. few E2E tests Compose UI tests: interactions Screenshot tests: components ร— states ร— themes Unit tests: ViewModels, reducers, mappers (most tests)
Most confidence should come from fast unit tests. UI and screenshot tests cover what unit tests can't see.

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

app/build.gradle.ktsGradle KTS
dependencies {
    androidTestImplementation(platform("androidx.compose:compose-bom:<latest>"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
}
LoginScreenTest.kt (androidTest)Kotlin
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

ToolHow it rendersNotes
Compose Preview Screenshot Testing (Google)Turns @Preview functions into screenshot tests, run on the JVMAlpha at the time of writing; setup is moving to AGP test suites. Check the current docs.
RoborazziJVM via RobolectricPopular open-source option; integrates with Robolectric tests
PaparazziJVM using layoutlibOpen source; no device needed
Device/emulator-basedOn real rendering stackMost 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:

Multi-preview annotation (shared)Kotlin
@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
src/screenshotTest/.../OrderCardScreenshots.ktKotlin
@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

  1. Commit reference images to the repository (or Git LFS).
  2. Run validation on every pull request, and publish the diff report as a CI artifact.
  3. When a change is intentional, regenerate references in the same PR so reviewers see the visual diff.
  4. 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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp