Published

Paparazzi screenshot tests without Git LFS

Arnold Noronha
Arnold Noronha
Founder, Screenshotbot

Paparazzi is one of the most popular screenshot testing libraries for Android. It renders your Views and Composables on the JVM with layoutlib, so there's no emulator or device to manage, and your screenshot tests run as fast as your unit tests.

But Paparazzi has to compare each screenshot against a golden image, and those golden images have to live somewhere. The Paparazzi README recommends storing them in your repository with Git LFS. For a handful of screenshots that works fine. As your team writes more screenshot tests, it starts to hurt.

In this post we'll look at why Git LFS gets painful as Paparazzi test suites grow, and how to run the same Paparazzi tests with Screenshotbot without storing a single PNG in your repository.

How Paparazzi works with Git LFS

The standard Paparazzi workflow looks like this:

  1. A developer changes some UI, and runs ./gradlew recordPaparazziDebug on their machine.
  2. Paparazzi writes the new golden images to src/test/snapshots/images.
  3. The developer commits the images along with the code. Git LFS stores the image contents on the LFS server, and leaves a small pointer file in the repository.
  4. CI runs ./gradlew verifyPaparazziDebug, which fails if any screenshot doesn't match its golden image.

To use LFS, you add a line like this to .gitattributes:

**/snapshots/**/*.png filter=lfs diff=lfs merge=lfs -text

This is a reasonable default. It keeps screenshots next to the code that produced them, and it doesn't need any extra infrastructure. But every one of those four steps gets worse as the number of screenshots grows.

Where it starts to hurt

Your repository only ever grows

Every time a screenshot changes, a new copy of the image is stored forever. Change a color in your design system and you may re-record hundreds or thousands of screenshots in one commit. LFS keeps the large files out of the Git packfiles, but they still count against your storage and bandwidth quota on GitHub, GitLab or Bitbucket, and they still have to be downloaded.

We wrote about this in more detail in Can Git LFS scale?

Every CI job pays for it

A CI job that runs verifyPaparazziDebug needs the golden images, so it has to fetch them from LFS. That's extra time and bandwidth on every build, including the builds that didn't touch any UI. CI caches help, but they're one more thing to tune and keep warm.

Recording on a laptop isn't the same as recording on CI

Paparazzi is much more deterministic than emulator-based screenshot tests, but the golden images are still recorded on whatever machine the developer happens to be using. Different JDK versions, operating systems and font rendering can produce pixel-level differences, which turn into failures on CI that nobody can reproduce locally. Teams usually end up with a "record in Docker" script, or a CI job that records and pushes a commit back to the branch.

Merge conflicts on binary files

Two branches that change the same screen will both change the same golden images. Git can't merge PNGs, so whoever merges second has to re-record and push again. On a busy repository with a shared design system, this happens a lot.

Reviewing changes is awkward

When a pull request changes fifty screenshots, the reviewer has to click through fifty image diffs in the code review tool, one file at a time, mixed in with the code changes. It's easy to miss the one screenshot that changed for the wrong reason.

A different approach: don't store screenshots in Git

Golden images are build artifacts. They're generated from your code, so they don't need to be stored next to it in version control: they only need to be stored somewhere you can compare against, and tied to the commit that produced them.

That's what Screenshotbot does. Instead of recording on a developer's machine and committing the result:

  1. CI runs Paparazzi in record mode on every commit, for both your main branch and your pull requests.
  2. The screenshots are uploaded to Screenshotbot, which stores them against the commit.
  3. For a pull request, Screenshotbot compares its screenshots against the ones from the merge base on your main branch, and reports the changes as a build status on the pull request.
  4. A reviewer looks through the changes side by side, with the differences highlighted, and accepts or rejects them.

Nothing is committed, so the repository stops growing, there's nothing to fetch from LFS on CI, and there are no merge conflicts on images. And because every screenshot is recorded on CI, they're all rendered in the same environment.

This scales to tens of thousands of screenshots, which is where the LFS approach tends to fall over.

We talked about this approach, and what it takes to scale up screenshot testing, at next app devCon 2026:

Scaling up your Screenshot Testing, without the friction

Setting up Paparazzi with Screenshotbot

You don't need to change your Paparazzi tests at all. The Screenshotbot Gradle plugin detects Paparazzi and adds the tasks you need.

First, add the plugin to your root build.gradle.kts:

plugins {
  id("io.screenshotbot.plugin") version "1.29.13"
}

Then apply it in each module that has Paparazzi tests:

apply(plugin = "io.screenshotbot.plugin")

Create an API key from your Screenshotbot account, and add it to your CI as the SCREENSHOTBOT_API_KEY and SCREENSHOTBOT_API_SECRET environment variables. Then replace verifyPaparazziDebug in your CI with:

./gradlew recordAndVerifyPaparazziDebugScreenshotbotCI

That's it. You'll see the screenshot changes on your next pull request. You may need to adjust the task name for your build flavor. The Android documentation has the full details, and there's a complete example in screenshotbot/paparazzi-example.

Moving your existing snapshots out of LFS

Once Screenshotbot is running on CI, you can delete the golden images from your repository. Remove the LFS rule for snapshots from .gitattributes, then:

git rm -r --cached '*/src/test/snapshots/*'
echo '**/test/snapshots' >> .gitignore
git commit -m "Move Paparazzi snapshots to Screenshotbot"

The .gitignore entry stops anyone from accidentally committing new snapshots by running the old record task. Older commits still reference the old images in LFS, so keep them around until you no longer need to build those commits.

Running screenshot tests locally

Developers can still check their changes before pushing. After a one-time ./gradlew :installScreenshotbot to set up an API key, record a baseline and then verify your changes against it:

./gradlew :app:recordPaparazziDebugScreenshotbot
# make your changes
./gradlew :app:verifyPaparazziDebugScreenshotbot

See local runs for how this works.

Wrapping up

Git LFS is a fine way to start with Paparazzi. But golden images behave differently from source code: they change in bulk, they can't be merged, and they need a visual review. Once you have more than a few hundred of them, it's worth moving them out of Git and into a tool built for them.

Screenshotbot works with Paparazzi, Roborazzi, Compose Preview Screenshot Tests and the other popular Android screenshot testing library, as well as iOS and web. Try it for free, or get in touch if you'd like help migrating a large Paparazzi suite.

Did you know that Screenshotbot is platform agnostic, and works for both iOS and Android teams?
See how Screenshotbot integrates with swift-snapshot-testing →

Did you enjoy this post? Share the knowledge!