Paparazzi: Android screenshot tests that skip the emulator entirely
Render your Android screens without a physical device or emulator
At a glance
- What is it?
- Cash App's Kotlin library renders Android layouts and Composables to PNGs on the JVM using LayoutLib, with Gradle tasks for recording and verifying golden images, plus JUnit 4, Jupiter and platform runners.
- Who is it for?
- Paparazzi is worth adopting if your team has skipped screenshot tests because emulator tests are too slow, because it turns them into ordinary JVM unit tests that need no device at all. The cost is fidelity: it renders through LayoutLib rather than a real device, so anything that depends on the actual graphics driver, real fonts installed on a system image, or a runtime permission will not match production.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A snapshot test that is an ordinary JVM unit test
Paparazzi's claim is in one sentence at the top of the README: an Android library to render your application screens without a physical device or emulator. The rendering happens through LayoutLib, the same layout engine Android Studio uses for previews, running on the JVM.
The JUnit 4 form is the shortest possible example of the pattern. You construct a `Paparazzi` rule with a device config and a theme, inflate a layout or instantiate a Composable, set its data, and take a snapshot:
class LaunchViewTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = PIXEL_5,
theme = "android:Theme.Material.Light.NoActionBar"
)
@Test
fun launchView() {
val view = paparazzi.inflate<LaunchView>(R.layout.launch)
view.setModel(LaunchModel(title = "paparazzi"))
paparazzi.snapshot(view)
}
}The Composable case is shorter still, because `snapshot` takes a lambda and you can call the Composable directly:
@Test
fun launchComposable() {
paparazzi.snapshot {
MyComposable()
}
}The `deviceConfig` argument is what makes a snapshot meaningful. Rendering the same screen at Pixel 5 dimensions catches layout problems that would never appear at one fixed size, which is the main reason to reach for a snapshot library rather than a single golden image.
Running, recording and verifying through Gradle tasks
Three Gradle tasks do all the work, and the README names them precisely:
./gradlew :sample:testDebug
./gradlew :sample:recordPaparazziDebug
./gradlew :sample:verifyPaparazziDebugThe first runs the tests and generates an HTML report at `sample/build/reports/paparazzi/` showing every test run and snapshot. The second records snapshots as golden values into a source-controlled location that defaults to `src/test/snapshots`. The third verifies against previously recorded values, and failures produce diffs at `sample/build/paparazzi/failures`.
That third path is the workflow to design around. A pull request that changes a colour produces a diff image rather than a stack trace, which is a different kind of review conversation: you look at the picture and decide whether the change was intended.
The test runner integration is unusually broad, judging by the repository tree. Alongside the core `paparazzi/` module there are `paparazzi-junit-jupiter/`, `paparazzi-junit-platform/` and `paparazzi-junit-vintage/`, which covers JUnit 5, the JUnit Platform and JUnit 4 respectively, plus `paparazzi-gradle-plugin/`, `paparazzi-annotations/`, and a `paparazzi-preview-runtime/`, `paparazzi-preview-processor/` and `paparazzi-preview-lints/` trio that appears aimed at Android Studio preview parity.
Release 2.0.0-alpha05, published 2026-05-20, added `setup` and `teardown` hooks on Paparazzi specifically to decouple it from JUnit 4, with documented usage for JUnit Jupiter. That change is what makes the library usable from a JUnit 5 test suite without the rule lifecycle getting in the way.
Git LFS is not optional when snapshots grow
Snapshot tests produce PNGs, and a few hundred of them will bloat a repository fast. The README recommends Git LFS and gives the setup verbatim:
brew install git-lfs
git lfs install --local
git lfs track "**/snapshots/**/*.png"
git add .gitattributesThe optional `git config lfs.setlockablereadonly false` line exists to improve checkout performance. The snapshot glob it tracks matches the default output directory, so a project that keeps goldens anywhere else needs its own pattern.
The interesting part is what follows. Because LFS stores content outside the git object store, it is possible to commit snapshot files that git thinks it is tracking but LFS is not, and those files will resolve to pointer text in any other checkout. The README addresses this with a `pre-receive` hook that diffs the two lists:
diff <(git ls-files ':(attr:filter=lfs)' | sort) <(git lfs ls-files -n | sort) >/dev/null
ret=$?
if [[ $ret -ne 0 ]]; then
echo >&2 "This remote has detected files committed without using Git LFS."
exit 1;
fiand a matching snippet for a build script that runs it before pulling LFS content. The failure this prevents is a genuinely nasty one: a CI job that silently verifies against pointer files, or a teammate whose local checkout has no images at all. If you adopt Paparazzi at scale, wire the hook up on day one.
Where the rendering differs from a real device
Two README sections exist purely because LayoutLib is not a device, and they are the most useful part of the documentation.
The first is Lottie. When snapshotting a Lottie animation you must force it to run on the calling thread, otherwise Paparazzi throws exceptions, with issues 494 and 630 cited:
@Before
fun setup() {
LottieTask.EXECUTOR = Executor(Runnable::run)
}The second is `LocalInspectionMode`. Composables such as `GoogleMap()` check that flag and short-circuit to a preview-safe placeholder. Paparazzi deliberately does not set it globally, so that a snapshot represents production output, on the same reasoning that it overrides `View.isInEditMode` for legacy views. The recommended workaround wraps the Composable:
@Test
fun inspectionModeView() {
paparazzi.snapshot(
CompositionLocalProvider(LocalInspectionMode provides true) {
YourComposable()
}
)
}Both are honest about a limitation that no amount of testing removes. A snapshot that renders a placeholder is not evidence about the real map, and a snapshot that captures mid-animation state is not evidence about the animation. Paparazzi verifies layout, text, colour and geometry well. Anything that depends on a real graphics driver, real system fonts or a runtime permission is out of scope, and a team should keep a smaller set of instrumented tests for those.
The 2.0 alpha line and what it demands of your build
Both published releases are alphas. 2.0.0-alpha04 came out on 2026-01-20 and its first line is a requirement: consumers must build on Java 21 or later. The rest of that release moved the toolchain forward, listing LayoutLib v16.1.1, Compose 1.10.1, Kotlin 2.3.0, Gradle 9.2.1 and Android Gradle Plugin 8.13.2. It also added configurable differs, additional differ types, a `defaultLocale` system property, custom accessibility hierarchy ordering in accessibility snapshots, and support for overwriting snapshots once a maximum percent difference threshold is reached.
2.0.0-alpha05, on 2026-05-20, states that it supports pre-AGP 9.0 consumers, which is the kind of compatibility note that tells you the project is straddling two Android toolchain generations. It brings LayoutLib v16.2.1, Compose 1.11.2 and Gradle 9.3.1, and includes a fix for a configuration-cache issue resolving `project` in a provider mapping.
That configuration-cache fix is worth pausing on, because it is the kind of bug that only appears in a large multi-module build. The repository has a `build-logic/` directory and a `sample/build.gradle`, so the plugin's interaction with complex Gradle setups is something being actively fixed rather than assumed to work.
Applying the plugin has two documented forms, the older `buildscript` block with a `classpath` dependency on `app.cash.paparazzi:paparazzi-gradle-plugin:2.0.0-alpha05`, or the plugins DSL with `id 'app.cash.paparazzi' version '2.0.0-alpha05'`. Development snapshots come from a Central Portal Maven snapshots repository. The repository also has `AGENTS.md` and `CLAUDE.md` files, which tells you something about how contributions are being made in 2026.
Where Paparazzi sits against emulator tests and Roborazzi
The realistic comparison is not with other screenshot libraries but with the two ways teams currently test Android UI.
The first is instrumented tests on an emulator or device. They are the ground truth, since they run the real graphics stack, real fonts and real permissions. They are also slow, expensive in CI minutes, and flaky in ways that have nothing to do with your code. Paparazzi's argument is not that it is better but that it is fast enough to run on every change, which changes how many screens you actually cover.
The second is Compose Preview screenshot testing, which builds on the tooling Android Studio already has. The `paparazzi-preview-*` modules in the tree suggest Paparazzi is competing here too, so existing `@Preview` functions can be snapshotted rather than rewritten. That is a meaningful difference from libraries that require you to introduce a new test wrapper, because a preview is something you already have.
The limitation to accept with either approach is that a snapshot test proves rendering did not change, not that the screen is correct. It will happily record a wrong layout as the new golden. That makes the record-versus-verify workflow the real control: recorded images should be reviewed in the pull request that changes them, with the diff images doing the work of showing what moved.
The licence is Apache 2.0, held by Square, Inc. with a 2019 copyright line, so there is no obstacle to using it in a commercial project.
Editorial conclusion
Paparazzi is worth adopting if your team has skipped screenshot tests because emulator tests are too slow, because it turns them into ordinary JVM unit tests that need no device at all. The cost is fidelity: it renders through LayoutLib rather than a real device, so anything that depends on the actual graphics driver, real fonts installed on a system image, or a runtime permission will not match production. Adopt the plugin, run `./gradlew :sample:verifyPaparazziDebug` in CI with Git LFS tracking the snapshot directory, and treat the `LocalInspectionMode` workaround in the README as a signal about which Composables deserve separate handling rather than as an afterthought.
Frequently asked questions
What is Paparazzi used for?
Rendering Android screens to images on the JVM, without a physical device or emulator. You snapshot a layout you inflate or a Composable you call, Paparazzi renders it through LayoutLib at a chosen device config, and the PNG becomes a golden file you commit and diff. That makes screenshot tests fast enough to run on every change rather than as a nightly job.
Which Gradle tasks do I use to record and verify Paparazzi snapshots?
`./gradlew :sample:recordPaparazziDebug` records golden images into a source-controlled directory that defaults to `src/test/snapshots`, and `./gradlew :sample:verifyPaparazziDebug` verifies against them, writing failure diffs to `sample/build/paparazzi/failures`. `./gradlew :sample:testDebug` runs the tests and produces an HTML report under `sample/build/reports/paparazzi/`.
Do I need an emulator or a connected device to run Paparazzi?
No. Paparazzi renders through LayoutLib on the JVM, so no device, emulator or instrumentation is involved. What it does need is a working JVM build of your Android code and a toolchain matching the release: 2.0.0-alpha04 onward requires Java 21 or later, and the alphas bundle specific LayoutLib, Compose and Gradle versions.
Why should Paparazzi snapshots be stored in Git LFS?
Because they are PNGs, and a few hundred of them will dominate repository size. The README recommends `git lfs track "**/snapshots/**/*.png"` and also gives a `pre-receive` hook that fails the push when files matching the LFS filter were committed without LFS. Without that check, a checkout can contain pointer files instead of images and verification fails in a way that is hard to diagnose.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/cashapp-paparazzi)