Sitemap
Press enter or click to view image in full size

Paparazzi vs Compose Screenshot Testing: A Practical Android Guide

9 min readOct 27, 2026

--

Screenshot testing helps answer a question that regular unit testing often cannot.

Do we have a UI that looks how we expect?

Composable components can return the correct state and pass all unit tests however, they may still display incorrect UI such as text being cut out of view, unexpected white space or spacing, incorrect colours or just generally broken UI. Using screenshot tests we can capture a UI and ensure it matches an approved version.

There are two approaches available right now to Android teams using Jetpack Compose. Compose Preview Screenshot Testing which integrates with your Android Gradle Plugin (AGP) test suites. The other option is Cash App’s Paparazzi.

Both options allow you to render Compose UIs without having to run the tests on a real device or emulator. However there are differences between the API’s and configurations, how everything is rendered and how the tools integrate into Gradle.

In this article we are going to compare both options. This will not tell you which one is best for you but will give you an insight into where they can be used and what impact they would have on your Android codebase.

1. Start with the same test scenario

Consider a product card that displays the product name, price and availability. We want to ensure that any changes made to this layout don't accidentally happen with typography, spacing, colours and components.

The composable is shared by both examples:

@Composable
fun ProductCard(
name: String,
price: String,
available: Boolean,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
Column(
modifier = Modifier.padding(16.dp),
) {
Text(
text = name,
style = MaterialTheme.typography.titleMedium,
)

Spacer(Modifier.height(8.dp))

Text(
text = price,
style = MaterialTheme.typography.bodyLarge,
)

Spacer(Modifier.height(12.dp))

Text(
text = if (available) "In stock" else "Out of stock",
color = if (available) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.error
},
)

}
}
}
Press enter or click to view image in full size
Product card preview

Both implementations should use the same ProductCard, MaterialTheme and inputs values to render the component in the way we expect.

Our baseline scenario is:

  • Product: Wireless Headphones
  • Price: $79.99
  • Availability: in stock
  • Theme: the application’s light theme
  • Expected result: a card with consistent typography, spacing, colors, and alignment

This means we can compare our results!

We are testing the same UI, not comparing two different implementations that happen to produce screenshots.

2. Option A: Compose Preview Screenshot Testing

Composable Preview Screenshot tests work by using composable previews as an input screenshot tests. They have their own test suite set up which includes Gradle tasks to generate, plus handling of the reference images.

The test suite API is still experimental. The DSL may change. The setup below follows the AGP test-suite approach, rather than the older standalone screenshot plugin configuration.

Official documentation: Screenshot testing with AGP test suites.

Step 1: Enable screenshot testing

Add the experimental properties to the root gradle.properties file:

android.experimental.enableScreenshotTest=true
android.experimental.testSuiteSupport=true

These flags enable screenshot testing and AGP test-suite support for this setup.

Step 2: Configure the screenshot-test suite

In the Android module’s build.gradle.kts, configure the suite inside android.testOptions:

android {
testOptions {
screenshotTests.create("screenshotTest") {
engineVersion = "0.0.1-alpha16"
targetVariants.add("debug")
dependencies {
implementation(
"androidx.compose.ui:ui-tooling:<compose-version>"
)
implementation(
"com.android.tools.screenshot:screenshot-validation-api:0.0.1-alpha16"
)
}
}
}
}

Replace <compose-version> with the Compose UI Tooling version compatible with your project. Keep the screenshot engine and validation API versions aligned.

This is just an example of how to generate using the documented API. Check for the latest stable version when implementing it.

The important thing to note here is that we are declaring our dependencies within the dependencies block of the screenshot suite. We aren’t adding a screenshot-test dependency to our modules general testImplementation configuration.

The documented setup requires Android Studio Rabbit 1 Canary 4 or newer, AGP 9.5.0-alpha03 or newer, Compose Screenshot Engine 0.0.1-alpha16 or newer and JDK 17 or newer. As these are parts of the experimental toolchain, ensure they are correct by checking the documentation.

Step 3: Add the preview test

Create the test in the dedicated source set:

src/screenshotTest/kotlin/.../ProductCardScreenshotTest.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.android.tools.screenshot.PreviewTest

@PreviewTest
@Preview(showBackground = true)
@Composable
fun ProductCardScreenshotTest() {
AppTheme {
ProductCard(
name = "Wireless Headphones",
price = "$79.99",
available = true,
)
}
}

@PreviewTest marks the composable preview as an input for screenshot testing. The regular @Preview annotation supplies the preview configuration.

This is not a typical JUnit test method that calls an assertion on a bitmap, but instead uses our screenshot test infrastructure to find the annotated preview, render it and compare its output against the stored reference image.

Step 4: Record and verify the reference

For a suite named screenshotTest targeting the debug variant, the documented task pattern is:

./gradlew updateScreenshotTestDefaultDebugTestSuite

To verify the screenshot against the recorded reference:

./gradlew testScreenshotTestDefaultDebugTestSuite

The Default segment is part of the generated task naming convention. If you change the suite name, target, or variant configuration, the actual task name may differ. Use the task list or the generated tasks for your configured project rather than assuming every project has the same task name.

Reference images are stored under the suite-and-variant-specific source-set path. Failed comparisons produce an HTML test report that helps identify missing references, size mismatches, and pixel differences.

What this approach gives you: a preview-oriented testing model integrated with AGP’s test-suite infrastructure, including suite-specific configuration and Gradle task generation.

3. Option B: Paparazzi

Paparazzi is an open-source screenshot-testing library from Cash App. It renders Android UI through Layoutlib, the host-side rendering engine used by Android Studio’s Layout Editor and Compose Preview. Layoutlib simulates enough of the Android framework to render UI on your development machine without requiring a physical device or emulator.

Paparazzi uses a different testing model: you create a snapshot using an instance of Paparazzi within your tests. The plugin will provide you with Gradle tasks to capture / assert the images.

In the example below we are using the version (1.3.5). There is also an alpha version of Paparazzi (2.0), therefore you should look at the release notes and ensure the version you choose is compatible with your AGP, Gradle, Kotlin and Compose versions.

Step 1: Add the plugin and dependency

In the module’s build.gradle.kts:

plugins {
id("app.cash.paparazzi") version "1.3.5"
}

dependencies {
testImplementation("app.cash.paparazzi:paparazzi:1.3.5")
}

The example uses the standard test source set and JUnit-based test execution. Unlike the AGP test-suite setup, Paparazzi does not require the screenshot-test suite DSL or its experimental properties.

The plugin and library versions must be the same. If you are using a version catalog then make sure to put the version there.

Step 2: Write the screenshot test

Create a test under the module’s conventional test source set, such as src/test/kotlin/.../ProductCardScreenshotTest.kt:

import app.cash.paparazzi.Paparazzi
import org.junit.Rule
import org.junit.Test

class ProductCardScreenshotTest {

@get:Rule
val paparazzi = Paparazzi()

@Test
fun productCard_inStock_matchesSnapshot() {
paparazzi.snapshot {
AppTheme {
ProductCard(
name = "Wireless Headphones",
price = "$79.99",
available = true,
)
}
}
}
}

Here, Paparazzi() is a JUnit rule, and snapshot renders the composable and captures its output. The test is a standard JUnit test, which means test organization and execution follow the conventional test infrastructure.

Get Ravi Rawal’s stories in your inbox

Join Medium for free to get updates from this writer.

The example assumes AppTheme and ProductCard are available to the test and that the module has the Compose dependencies required to compile them.

Step 3: Record and verify the reference

For a module named sample, Paparazzi provides tasks such as:

./gradlew :sample:recordPaparazziDebug

This records the current rendered output as the approved snapshot.

Run the normal test task to execute the screenshot test:

./gradlew :sample:testDebug

Then verify against the saved snapshots:

./gradlew :sample:verifyPaparazziDebug

The exact task paths depend on the module and Android variant. Paparazzi by default stores snapshots under src/test/snapshots; failures produce image diffs under the module's build directory.

What this approach gives you: a test-first API that fits into a conventional Android/JUnit test suite, with explicit snapshot recording and verification tasks.

Press enter or click to view image in full size

4. Compare the workflows side by side

Both tools can test the same ProductCard. The main difference is how a team defines a screenshot test and integrates it into the build.

Press enter or click to view image in full size

5. Same record–verify cycle, implemented differently

A fair comparison follows the same sequence in both tools.

Record a known-good image

With Compose screenshot’s test-suite integration, the update task finds the annotated preview, renders it. Then the reference image will be captured.

In Paparazzi, the record task executes the test and stores the snapshot produced by paparazzi.snapshot.

You should always check the captured reference image before comitting. Capturing a screenshot does not guarantee your UI is correct. It only establishes the current output as the baseline.

Change the UI

Imagine we have changed the inner padding of the card to 8.dp from 16.dp.

The product name, price, and availability are still correct, so a unit test could pass. But the screenshot test should fail as the layout has changed.

Neither tool knows whether the change is a design improvement or a regression. It will just show there is a visual difference for the developer to review.

Press enter or click to view image in full size

Verify the image

Run the relevant verify task in CI. If the rendered image does not match the already approved image within the tolerances(configured comparison rules) set then the test will fail. The diff will be evidence for review.

The workflow is conceptually identical:

  1. Render a deterministic UI state.
  2. Compare it with an approved image.
  3. Inspect the difference.
  4. Accept the change by updating the reference, or fix the regression.

The implementation and task names differ, but the engineering discipline should remain the same.

6. Rendering determinism matters more than the API

A screenshot test will only be useful if your results are stable.

Never pass dynamic values like API responses, timestamps, random values or arbitrary animation frames. You should always pass static data along with a specific state you wish to guard against.

Also pay attention to:

  • Theme: light/dark themes can have very different outputs and will affect the results.
  • Dimensions: text wrapping and layout may change based on the available width and height.
  • Fonts: font availability and rendering differences can create noisy image diffs.
  • Density and configuration: pixel output will depend on the render configuration.
  • Dependencies and toolchain: renderer and library upgrades can cause changes in output even when application code has not changed.

For the acc comparison, run the same scenario on a controlled CI environment and treat changes to rendering dependencies as potentially baseline-affecting changes.

7. What screenshot tests should — and should not — replace

Screenshot tests are a great way to test the visual contract of your components. Your design system components, cards, dialogs, empty states, loading states, error states, or any complex type or layout or combinations may we validated with visual tests.

However, they do not replace behavioral tests. You cannot verify that the button is working correctly, a navigation event is triggered, a repository handles an error, or an accessibility action is performed using a visual test.

Use the right test for each concern:

  • Unit tests verify business logic and state transformations.
  • Screenshot tests detect unintended visual changes in controlled UI states.
  • Instrumented UI tests verify behavior that depends on runtime interaction, device APIs, navigation, or integration with the Android framework.

A balanced test suite uses screenshot tests to cover visual contracts without trying to turn every screen into a golden-image test.

8. Which should you choose?

Compose Preview Screenshot Testing: This is a great option if your team are looking for a preview centric model and are happy to use the latest experimental AGP and engine versions. The integration with AGP test suites gives you suite-level configuration and generated tasks that fit the Gradle testing lifecycle.

Paparazzi: This provides a more traditional JUnit approach with clear calls to take screenshots, and an established record/verify workflow. This is also a good choice if you already have screenshot testing in your build and want to add support for testing compose views.

Either option will still require you to have consistent test data, well-managed reference updates, clean CI environments and manual validation of visual changes.

Before standardizing either of these tools at scale across your Android codebase, run some small-scale testing using a few sample components (one simple card, one long text screen and one conditional state). This can help give a good indication of stability, performance, failure analysis, execution time, compatibility with Gradle and maintenance of references when making regular UI changes.

The best choice is the one your team can operate consistently — not simply the one with the shortest test example.

References

--

Ravi Rawal
Ravi Rawal

Written by Ravi Rawal

Principal Engineer sharing insights on engineering leadership, mobile architecture, developer productivity, automation, and building high-performing teams.