Screenshot Testing: Paparazzi, Roborazzi & Visual Regression
Catch pixel-level regressions before your users do — master every major Android screenshot testing framework
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Screenshot testing (also called visual regression testing) is an automated testing approach where you capture rendered UI as image files, store approved versions as golden baselines, and compare future renders against those baselines to detect unintended visual changes. On Android, frameworks like Paparazzi (JVM-based, no emulator), Roborazzi (Robolectric-based), and Dropshots (device-based) automate this process, enabling teams to catch broken layouts, wrong colors, missing icons, and theme inconsistencies before they ship to users.
Real-world relevance
At Cash App, Paparazzi was created because their design system team needed to validate hundreds of UI components across themes and screen sizes without maintaining a fleet of emulators. Companies like Dropbox, Square, and Google use screenshot testing to enforce design consistency across feature teams — when any PR changes a shared component, the screenshot tests immediately show which screens are affected. In large-scale apps with 50+ contributors, screenshot testing is the primary defense against 'it looked fine on my machine' visual bugs.
Key points
- Why Screenshot Testing Matters — Traditional unit and integration tests validate logic but miss visual regressions — a wrong color, broken padding, overlapping text, or theme inconsistency. Screenshot tests capture rendered UI as images and compare them against approved baselines (golden images), catching visual bugs that functional tests completely ignore.
- Paparazzi by Cash App — JVM-Based Speed — Paparazzi runs entirely on the JVM with no emulator or device required. It uses LayoutLib (the same renderer Android Studio uses for previews) to inflate Views and Compose UI, then captures them as PNG files. This makes it extremely fast — a suite of 500 screenshot tests can run in under 60 seconds. You record golden images with `./gradlew recordPaparazziDebug` and verify with `./gradlew verifyPaparazziDebug`.
- Paparazzi DeviceConfig & Multi-Config Testing — Paparazzi supports DeviceConfig presets (PIXEL_5, NEXUS_5, etc.) and lets you parameterize tests across screen sizes, font scales, dark/light themes, and locales including RTL. A single test class with @RunWith(TestParameterInjector::class) can generate dozens of golden images covering your entire device matrix without a single emulator boot.
- Roborazzi — Robolectric-Powered Screenshots — Roborazzi leverages Robolectric's native graphics mode (@GraphicsMode(GraphicsMode.Mode.NATIVE)) to render real Android UI on the JVM. It supports three modes: `record` (save new baselines), `compare` (generate diff images highlighting changes), and `verify` (fail if differences exceed threshold). Use `captureRoboImage()` on any View, Composable, or even ActivityScenario.
- Dropshots by Dropbox — Dropshots provides a simpler API focused on tolerance-based comparison. It runs on real devices/emulators via instrumentation tests but offers a Gradle plugin that manages golden image storage and comparison. Its key feature is configurable pixel tolerance — you set a percentage threshold (e.g., 0.1%) below which differences are treated as acceptable anti-aliasing noise rather than real regressions.
- Compose Preview Screenshot Testing (2025+) — Google's official Compose Preview Screenshot Testing lets you annotate @Preview composables and automatically generate screenshot tests from them. Combined with ComposablePreviewScanner and AndroidUiTestingUtils by Sergio Sastre, you can scan all @Preview functions in your codebase and generate parameterized screenshot tests for each — turning your preview catalog into a visual regression suite with near-zero boilerplate.
- Golden Image Management & Git Strategy — Golden images (baselines) are typically stored in the repo under a dedicated directory (e.g., `src/test/snapshots`). When UI intentionally changes, you re-record baselines and commit the updated PNGs. Best practice: use Git LFS for large image sets, require screenshot diff review in PRs, and keep golden images organized by test class name and device config for easy navigation.
- CI/CD Integration Patterns — In CI pipelines, screenshot tests run in `verify` mode — comparing current renders against committed baselines. Failed tests produce diff images showing exactly what changed. Advanced setups upload diff artifacts to PR comments (using Danger, GitHub Actions artifacts, or custom bots) so reviewers see visual changes inline. JVM-based tools (Paparazzi, Roborazzi) are CI-friendly since they need no emulator.
- Tolerance, Thresholds & Anti-Aliasing — Pixel-perfect comparison is often too strict — different JVM versions, rendering engines, or OS updates cause sub-pixel anti-aliasing differences. Most frameworks support tolerance thresholds: Paparazzi uses a max-percent-difference, Roborazzi supports custom comparators with pixel tolerance, and Dropshots has built-in percentage-based thresholds. A common sweet spot is 0.05-0.5% tolerance to catch real bugs while ignoring rendering noise.
- Multi-Configuration Matrix Testing — Production apps must look correct in dark mode, light mode, RTL layouts, large font accessibility settings, and various screen densities. Screenshot testing frameworks support parameterized runs across all these configurations. A single Composable can be tested in 8+ configurations automatically — dark+LTR, dark+RTL, light+LTR, light+RTL, each at normal and large font scale — creating a comprehensive visual safety net.
- Framework Comparison: Choosing the Right Tool — Paparazzi: JVM-only, fastest, great Compose support, Cash App maintained. Roborazzi: JVM via Robolectric, supports Activities/Fragments, good diff visualization. Dropshots: device-based, simple API, tolerance-focused. Android Testify: device-based, mature, good for legacy View code. Shot by Pedrovgs: device-based, widely used. For new Compose-first projects, Paparazzi or Roborazzi are the top choices; for legacy View-heavy apps needing real device rendering, Dropshots or Testify.
- AI-Assisted & Emerging Trends — Emerging tools use ML to distinguish meaningful visual changes from noise (anti-aliasing, shadow rounding). Services like Percy and Applitools (cross-platform) offer AI-powered visual comparison. In the Android ecosystem, expect tighter Compose Preview integration, IDE-inline diff previews, and smarter tolerance algorithms that understand semantic UI regions rather than raw pixel comparison.
Code example
// ============================================
// 1. PAPARAZZI SETUP — build.gradle.kts (module)
// ============================================
plugins {
id("app.cash.paparazzi") version "1.3.4"
}
// ============================================
// 2. PAPARAZZI TEST — Compose Component
// ============================================
class PaymentCardScreenshotTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5,
theme = "android:Theme.Material3.DayNight",
renderingMode = SessionParams.RenderingMode.NORMAL,
maxPercentDifference = 0.1 // 0.1% tolerance
)
@Test
fun paymentCard_defaultState() {
paparazzi.snapshot {
MyAppTheme(darkTheme = false) {
PaymentCard(
amount = "$42.00",
recipient = "Jane Doe",
status = PaymentStatus.COMPLETED
)
}
}
}
@Test
fun paymentCard_darkMode() {
paparazzi.snapshot {
MyAppTheme(darkTheme = true) {
PaymentCard(
amount = "$42.00",
recipient = "Jane Doe",
status = PaymentStatus.PENDING
)
}
}
}
}
// ============================================
// 3. PARAMETERIZED MULTI-CONFIG TEST
// ============================================
@RunWith(TestParameterInjector::class)
class DesignSystemScreenshotTest(
@TestParameter val darkMode: Boolean,
@TestParameter val isRtl: Boolean,
@TestParameter val fontScale: FontScale
) {
enum class FontScale(val value: Float) {
NORMAL(1.0f), LARGE(1.3f), HUGE(1.5f)
}
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5.copy(
fontScale = fontScale.value,
layoutDirection = if (isRtl)
LayoutDirection.RTL else LayoutDirection.LTR
)
)
@Test
fun primaryButton_allConfigs() {
paparazzi.snapshot {
MyAppTheme(darkTheme = darkMode) {
PrimaryButton(
text = "Submit Payment",
onClick = {}
)
}
}
}
// Generates 12 screenshots:
// 2 themes x 2 directions x 3 font scales
}
// ============================================
// 4. ROBORAZZI — Robolectric Screenshot Test
// ============================================
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [33])
class ProfileScreenRoborazziTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun profileScreen_loaded() {
composeRule.setContent {
MyAppTheme {
ProfileScreen(
user = User("Alex", "alex@dev.com"),
isEditing = false
)
}
}
composeRule.onRoot()
.captureRoboImage(
filePath = "src/test/snapshots/profile_loaded.png",
roborazziOptions = RoborazziOptions(
compareOptions = CompareOptions(
changeThreshold = 0.01 // 1% threshold
)
)
)
}
@Test
fun profileScreen_editMode_diff() {
composeRule.setContent {
MyAppTheme {
ProfileScreen(
user = User("Alex", "alex@dev.com"),
isEditing = true
)
}
}
// In compare mode, generates a 3-panel diff:
// [baseline] [actual] [highlighted diff]
composeRule.onRoot()
.captureRoboImage("src/test/snapshots/profile_edit.png")
}
}
// ============================================
// 5. COMPOSE PREVIEW SCREENSHOT TESTING
// ============================================
// In your UI module — define previews as usual
@Preview(name = "Light")
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun OrderSummaryPreview() {
MyAppTheme {
OrderSummary(
items = listOf("Burger", "Fries"),
total = "$12.99"
)
}
}
// In your test module — auto-scan and test all previews
@RunWith(TestParameterInjector::class)
class ComposePreviewTests(
@TestParameter(valuesProvider = PreviewProvider::class)
val preview: ComposablePreview<AndroidPreviewInfo>
) {
@get:Rule
val paparazzi = Paparazzi()
class PreviewProvider : TestParameter.TestParameterValuesProvider {
override fun provideValues(): List<ComposablePreview<AndroidPreviewInfo>> =
AndroidComposablePreviewScanner()
.scanPackageTrees("com.myapp.ui")
.getPreviews()
}
@Test
fun previewScreenshot() {
paparazzi.snapshot {
preview()
}
}
}
// ============================================
// 6. CI WORKFLOW — GitHub Actions
// ============================================
// .github/workflows/screenshot-tests.yml
//
// name: Screenshot Tests
// on:
// pull_request:
// paths: ['app/src/**', 'design-system/**']
//
// jobs:
// verify-screenshots:
// runs-on: ubuntu-latest
// steps:
// - uses: actions/checkout@v4
// with:
// lfs: true # golden images in Git LFS
//
// - uses: actions/setup-java@v4
// with:
// java-version: '17'
// distribution: 'temurin'
//
// - name: Verify Paparazzi Screenshots
// run: ./gradlew verifyPaparazziDebug
//
// - name: Upload diff artifacts on failure
// if: failure()
// uses: actions/upload-artifact@v4
// with:
// name: screenshot-diffs
// path: '**/build/paparazzi/failures/'
//
// - name: Comment PR with diff images
// if: failure()
// uses: marocchino/sticky-pull-request-comment@v2
// with:
// message: |
// Screenshot tests failed!
// Download diff artifacts to see changes.Line-by-line walkthrough
- 1. We start with the Gradle plugin declaration `id("app.cash.paparazzi")` which adds the Paparazzi framework to our module — this gives us the Paparazzi test rule and the record/verify Gradle tasks.
- 2. The `PaymentCardScreenshotTest` class demonstrates basic Paparazzi usage. The `@get:Rule val paparazzi = Paparazzi(...)` creates a JUnit rule that handles the LayoutLib rendering engine lifecycle — no emulator needed.
- 3. In the Paparazzi constructor, `DeviceConfig.PIXEL_5` sets the virtual screen dimensions, density, and form factor. `maxPercentDifference = 0.1` means up to 0.1% pixel difference is tolerated before a test fails.
- 4. The `paparazzi.snapshot { ... }` block takes a Composable lambda. Paparazzi renders this Composable using Android's LayoutLib (the same engine Android Studio uses for previews) and saves it as a PNG file.
- 5. The `DesignSystemScreenshotTest` uses `@RunWith(TestParameterInjector::class)` with `@TestParameter` annotations to create a cartesian product of all parameter combinations — 2 themes x 2 directions x 3 font scales = 12 test runs from one test method.
- 6. The `DeviceConfig.PIXEL_5.copy(fontScale = ..., layoutDirection = ...)` call creates a modified device configuration for each parameter combination, letting us test accessibility and RTL scenarios without any emulator setup.
- 7. In the Roborazzi test, `@GraphicsMode(GraphicsMode.Mode.NATIVE)` tells Robolectric to use native graphics rendering instead of shadow implementations — this is essential for accurate screenshot capture.
- 8. The `captureRoboImage()` extension function captures the current state of a Compose node or View and saves it to the specified file path. The `RoborazziOptions` configure comparison behavior including the change threshold.
- 9. The Compose Preview Screenshot Testing section shows how `AndroidComposablePreviewScanner().scanPackageTrees("com.myapp.ui")` automatically discovers all @Preview composables in the given package, converting your existing preview catalog into a screenshot test suite.
- 10. The CI workflow runs `verifyPaparazziDebug` which compares current renders against committed golden images. On failure, the `upload-artifact` step preserves diff images (showing baseline, actual, and highlighted differences) for developer review.
- 11. The `lfs: true` option in the checkout step ensures Git LFS files (golden images) are downloaded — without this, CI would have only LFS pointer files and all screenshot comparisons would fail.
- 12. The sticky PR comment step provides immediate visual feedback to developers when screenshots change, eliminating the need to manually download and inspect diff artifacts for every failed test.
Spot the bug
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33])
class HomeScreenScreenshotTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun homeScreen_defaultState() {
composeRule.setContent {
MyAppTheme {
HomeScreen(viewModel = HomeViewModel())
}
}
composeRule.onRoot()
.captureRoboImage("snapshots/home_default.png")
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Paparazzi GitHub — Official Repository & Documentation (Cash App / GitHub)
- Roborazzi GitHub — Robolectric Screenshot Testing (takahirom / GitHub)
- Screenshot Testing Compose UI — Android Developers Guide (Android Developers)
- Snapshot Testing on Android — Droidcon Talk (Droidcon)
- Android Screenshot Testing Comparison — Sergio Sastre (Hashnode)
- Dropshots GitHub — Dropbox Screenshot Testing (Dropbox / GitHub)