Roborazzi: screenshot tests that run on the JVM, not on a phone
Make JVM Android integration test visible 🤖📸
At a glance
- What is it?
- Roborazzi captures screenshots of Android and Compose interfaces from tests that run on a normal machine, and with Robolectric Native Graphics support it does so while Robolectric is still mocking the framework underneath. The result is visual regression testing in the same test task that already runs.
- Who is it for?
- Roborazzi fits an Android team that wants visual regression coverage without a device farm, that already uses Robolectric, and that wants the screenshots recorded, compared and verified inside the ordinary test task.
- 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 last received commits 2 days ago.
- 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 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library is for
Roborazzi is described in one line as making the JVM Android integration test visible, and the current headline is that it now supports Robolectric Native Graphics, and enables screenshot testing.
The argument for screenshot testing is in the README and it is a pragmatic one. Screenshot testing is key to validate that an application's appearance and functionality are right. It detects visual issues efficiently, and it tests the application as a user would use it. And it is quicker than writing many assertion statements, which is the practical point, because a screen that has ten visual states takes ten screenshots and one comparison, where the same coverage as assertions is a lot of code that nobody writes.
The library is Kotlin, it is published on Maven Central, and the homepage is `takahirom.github.io/roborazzi`. The recent releases are 1.76.0 from 2026-09-29, 1.75.0 from 2026-09-21 and 1.74.0 from 2026-09-08, so the release line moves every week or two, and the last push was on 2026-10-06.
The repository is much larger than a single library, and the root directory explains why. There is `roborazzi/` itself, `roborazzi-compose/` for the Compose side, `roborazzi-junit-rule/`, `roborazzi-idea-plugin/`, `roborazzi-painter/`, `roborazzi-accessibility-check/`, and two modules that look like experiments with models, `roborazzi-ai-openai/` and `roborazzi-ai-gemini/`. There are also a long list of `sample-*` directories, one for each way of using it, and directories for the proposal documents, `proposals/`, the agent skills, `skills/`, and an `include-build/` directory.
That is a repository with a plugin, a library, an IDE plugin, two model integrations, a documentation system and a dozen samples, and the version numbers suggest it is developed in the open rather than released once.
Why the JVM, and what device tests cost you
The README explains the choice of environment in a way that anyone who has run an Android test suite will recognise.
JVM tests, also known as local tests, are placed in the `test/` directory and run on a developer's PC or in a CI environment. Device tests, also known as instrumentation tests, are written in the `androidTest/` directory and run on real devices or emulators. And the consequence of that environment is frequent failures, leading to false negatives, which are often hard to reproduce and therefore tough to resolve.
That last sentence is the whole argument. A screenshot test that fails on one CI runner and passes on the next is worse than no screenshot test, because it teaches the team to re-run the build. Moving the rendering to the JVM removes the device from the equation, which removes the flake with it.
The cost of the move is that the rendering is a simulation, which is a point the library is careful about. Robolectric mocks the Android framework, and Roborazzi now supports Robolectric Native Graphics, which is the mode where the graphics calls are executed by native code rather than stubbed, so that what is captured looks like the real thing. The documentation is explicit about the requirement: to take screenshots you need Robolectric 4.10 alpha 1 or later, and the test class needs the native graphics mode.
@GraphicsMode(GraphicsMode.Mode.NATIVE)So the design is: keep the tests where they already are, in the JVM test task, and make the framework's rendering real enough that a screenshot is worth comparing.
The gap next to Paparazzi
The comparison with Paparazzi is the most useful paragraph in the README, because it explains the design by contrast.
Paparazzi is a great tool for visualizing displays within the JVM. However, it is incompatible with Robolectric, which also mocks the Android framework. Roborazzi fills that gap: it integrates with Robolectric, allowing tests to run with Hilt and to interact with components, and essentially it enhances Paparazzi's capabilities by capturing the screenshots with Robolectric.
The incompatibility is the whole issue. Paparazzi renders a display with its own implementation of the graphics stack, which means it cannot run inside a test that relies on Robolectric's mocking of the framework. A project that has thousands of Robolectric tests, with Hilt injection, with a mocked repository layer, cannot add Paparazzi on the side, because the two cannot be in the same test. Roborazzi sits inside the Robolectric test instead.
The Hilt point matters more than it first appears. A screenshot test that cannot use the project's real dependency injection is a screenshot test of a different application from the one that ships, and the value of the screenshot drops. Running inside Robolectric means the test uses the same components, the same module graph and the same fakes as the rest of the suite.
The README also shows this used as an architectural decision rather than a test-time addition. In the DroidKaigi 2023 application, Roborazzi was introduced from the early stages of development as part of the architectural design, which allowed the team to verify changes throughout the development process, and the reasoning is written up in that application's own repository.
The plugin creates three tasks, or you can set a property
The build integration is a Gradle plugin, and the plugin simply creates the tasks record, verify and compare, passing the configuration to the test.
Applied through the plugins DSL, the root build file declares it with `apply false` so that the version is centralised, and the module build file applies it:
plugins {
id("io.github.takahirom.roborazzi")
}The tasks that appear are the three verbs of a visual regression workflow. `recordRoborazziDebug` records a screenshot, and the default output directory is `build/outputs/roborazzi`, with a report under `build/reports/roborazzi/index.html`. `compareRoborazziDebug` reviews changes made to an image, comparing the current image with the saved one and generating a comparison image labelled as `[original]_compare.png`, along with a JSON file containing the diff information under `build/test-results/roborazzi`. And `verifyRoborazziDebug` is the one that fails the build when an image does not match.
The interesting part is that the dedicated tasks are not the only route. The same behaviour is available from the default unit test task, by adding `roborazzi.test.record=true` to `gradle.properties`, or passing the flag on the command line, for example `./gradlew testDebugUnitTest -Proborazzi.test.record=true`.
That is a design decision with a purpose. A team that already has a `testDebugUnitTest` task in its CI does not have to add a second task, and the screenshot behaviour becomes a property of the test run rather than a separate command. For a team that wants visual checks on every pull request, the difference between a flag and a new task is the difference between one line of configuration and a new build step to maintain.
The sample applications are the manual
The repository carries more than a dozen sample applications, and reading their names tells you which combinations the library is expected to work in.
There is `sample-android/`, `sample-android-without-compose/`, `sample-android-multiplatform/`, `sample-compose-v2/`, `sample-compose-desktop-jvm/`, `sample-compose-desktop-multiplatform/`, `sample-generate-preview-tests/`, `sample-generate-preview-tests-cross-runtime/` and `sample-generate-preview-tests-multiplatform/`. The desktop side has `roborazzi-compose-desktop/` and `roborazzi-compose-desktop-preview-scanner-support/`, the Compose multiplatform side has `roborazzi-compose/`, `roborazzi-compose-ios/` and `roborazzi-compose-preview-scanner-support/`, and there is `roborazzi-annotations/` and `roborazzi-junit-rule/` for the test-side integration.
The `sample-generate-preview-tests` family is worth singling out, because preview tests are the other way of doing this. In Compose, a preview function renders a component in isolation, and generating tests from previews means every component in the design system gets a screenshot without anybody writing a test. The cross-runtime and multiplatform variants exist because the previews have to be rendered somewhere, and the same preview may need to be captured on the JVM and on a different runtime, which is a different problem from running a test on a device.
The plugin support follows from that. The root file lists `roborazzi-idea-plugin/`, an IDE plugin, and `roborazzi-accessibility-check/`, which suggests the project reaches beyond screenshots into the rest of the checking a UI needs.
For a reader deciding whether to adopt the library, the samples are the documentation that matters, and they are chosen to cover the awkward combinations rather than the easy one. The `sample-android-without-compose/` directory is the one to look at first if the codebase is not Compose, and `sample-compose-v2/` if it is.
Documentation as an agent skill, and the cost of that choice
One feature of the project is worth its own paragraph, because it says something about where the project believes its users are.
Roborazzi ships its documentation as an installable agent skill, and one command installs the docs into an AI coding agent, so the agent can look up Roborazzi setup and usage while working on the project:
npx skills add takahirom/roborazziThe list of agents is specific: Claude Code, Cursor, Codex, Android Studio Agent Mode, Antigravity and Gemini CLI. The same repository has modules named `roborazzi-ai-openai/` and `roborazzi-ai-gemini/`, and a `.coderabbit.yaml` at the root for code review automation, and an `AGENTS.md`.
So the project treats the coding agent as a first-class consumer of its documentation, and it has gone further and built integrations for the models themselves. That is a coherent position for a project whose users are increasingly agents writing the test code, and it explains the `skills/` directory in the root and the `docs/` directory being the source of the skill.
It also explains a constraint. The README is generated, with comments at the top saying it is generated by a gradle task and by a document in the docs directory, and that the files should not be edited directly. The same pattern is visible in the sections marked as topic blocks, where the try it out, build setup and related content are all generated from separate sources. A reader of the repository is reading an assembled document, and the assembly is part of the build.
The `proposals/` directory and the `include-build/` directory point at the same practice from the other end, where the design of a new feature is written down before it is built, and where a composite build pulls in another build for the parts that are shared.
Where Roborazzi is the wrong tool
The limits follow from what the tool does, and the first one is about what the screenshots mean.
Roborazzi captures a rendering produced with the Android framework mocked by Robolectric, even in the native graphics mode where the graphics calls run as native code. That rendering is faithful enough to catch a layout regression, a wrong colour or a missing padding, and it is not a photograph of a device. A team whose visual requirements are pixel-exact, whose design has per-device rendering differences, or whose problem is a vendor specific behaviour, will find the screenshot agrees with the reference while the device disagrees. For those cases the answer is still an instrumented test on real hardware, and the honest description of Roborazzi is that it moves the check earlier and makes it cheaper, not that it replaces the device.
The second is the dependency on Robolectric. The library is dependent on Robolectric, and a project without it is not adding a screenshot library, it is adding a test framework and a screenshot library. The value of Roborazzi is highest in a codebase that already runs its logic tests on the JVM with Robolectric, and lowest in one that has no JVM test infrastructure, where the setup cost is the whole thing.
The third is the scope of a single screen. A screenshot test verifies pixels, and it does not verify behaviour, so a screen that renders correctly and responds wrongly passes. The README's claim that screenshot testing validates appearance and functionality is true in the sense that a broken interaction often shows up in the picture, and it is not a substitute for the interaction test that already exists in the suite.
The alternative approach, for a team that wants visual regression but not this stack, is Paparazzi, which does the same job in the JVM without Robolectric and therefore without the Hilt and mocking story, and the difference in approach is exactly the point of Roborazzi. A team whose tests are pure functions over a view model can use either. A team whose tests need the framework mocked can use one of them.
Editorial conclusion
Roborazzi fits an Android team that wants visual regression coverage without a device farm, that already uses Robolectric, and that wants the screenshots recorded, compared and verified inside the ordinary test task. It is the wrong tool for a team that needs pixel-accurate output from real hardware, since what it captures is a rendering produced with the framework mocked, and it is the wrong tool for a codebase with no JVM test infrastructure at all, because the whole approach depends on the test task existing. Before adopting it, read the sample applications in the repository, because the plugin setup differs between the Compose and the plain Android samples, and pin the Robolectric version, since the native graphics mode needs 4.10 alpha 1 or later.
Frequently asked questions
What is Roborazzi?
It is a Kotlin library that makes JVM Android integration tests visible by capturing screenshots of the interface, and it now supports Robolectric Native Graphics so that screenshot testing works while Robolectric is still mocking the Android framework.
Why run screenshot tests on the JVM instead of on a device?
Because device tests live in the androidTest directory and run on real devices or emulators, where the environment causes frequent failures and false negatives that are hard to reproduce. JVM tests live in the test directory and run on the developer machine or in CI, which removes the device from the equation.
How do I record, compare and verify screenshots with Roborazzi?
The Gradle plugin adds recordRoborazziDebug, compareRoborazziDebug and verifyRoborazziDebug tasks, and the same behaviour can be triggered from the default unit test task with roborazzi.test.record, roborazzi.test.compare or roborazzi.test.verify in gradle.properties, or as a -P flag on the command line.
How does Roborazzi differ from Paparazzi?
Paparazzi visualizes displays within the JVM but is incompatible with Robolectric, which also mocks the Android framework. Roborazzi integrates with Robolectric, so tests can run with Hilt and interact with components, and the screenshots are captured through it.
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/takahirom-roborazzi)Was this useful?