Snapshot testing is a method of automated user interface verification where the current screen state is compared with a reference image (snapshot) saved during the previous test run. Any visual discrepancy is recorded as a change requiring developer confirmation. Unlike UI tests that check for element presence, snapshot tests detect pixel-level changes — shifts, color deviations, and layout issues. According to Android Developers, 2024, snapshot testing detects up to 30% of visual regressions missed by traditional UI tests, making it an indispensable tool for maintaining a consistent interface.
Key Takeaways
Snapshot testing is a technique where a test renders a UI component, saves the resulting image as a reference, and on subsequent runs compares the current render against this reference. If the images match — the test passes. If differences are found — the test fails, and the developer receives a diff image highlighting the changed pixels. The technique was borrowed from web development (Jest snapshots) and adapted for mobile platforms.
The main value of snapshot tests is automatic detection of unexpected visual changes. A developer might change the color scheme in a global theme and accidentally affect a dozen screens. UI tests checking for button and text presence will not notice this. A snapshot test will capture every pixel change on every affected screen, providing a complete picture of the change impact.
According to the Mobile DevOps Summit 2023 survey, teams using snapshot testing in addition to classic UI tests reduce the number of visual defects in releases by 40%. This approach is especially effective in projects with design systems and component-based architectures, where changing one base component can affect dozens of application screens.
The fundamental difference lies in the object of verification. UI tests check the presence, state, and behavior of interface elements: “the button is visible”, “the text contains an error message”, “pressing opens a new screen”. Snapshot tests check the entire visual appearance: element positioning, margins, colors, fonts, shadows, and rounded corners. A snapshot test answers the question “does the screen look as expected?”, while a UI test answers “does the screen work as expected?”
Execution speed also differs. UI tests run on an emulator or real device, require full application loading, and take from 10 seconds to a minute per scenario. Snapshot tests using libraries like Paparazzi render components in a virtual environment without launching an emulator, reducing test time to 100–500 milliseconds. A full set of snapshot tests (50–100 screens) executes in 2–5 minutes instead of 30–60 minutes for a comparable set of UI tests.
However, snapshot tests do not replace UI tests. The optimal strategy is a combination: snapshot tests cover visual regression (rendering each screen in basic states), while UI tests cover behavioral aspects (click scenarios, input validation, navigation). This combination provides 90% confidence in interface correctness with minimal CI run time.
On Android, the main tools are Paparazzi and Shot. Paparazzi from Cash App renders components in a JVM test environment without an emulator, using Layoutlib gravity layout. Shot from Karumi performs Instrumentation screenshots on a real device or emulator and compares them against references using the AShot library, accounting for differences in resolution and pixel density.
Paparazzi does not require launching an emulator — rendering is done on the JVM through Layoutlib, providing speed comparable to unit tests. The library supports both the View system and Jetpack Compose. For Compose, use the paparazzi.snapshot { MyComposable() } modifier. References are stored in src/test/snapshots and are automatically compared on each run. The maximum difference percentage is configurable via maxPercentDifference.
SnapshotTesting from Point-Free supports comparison of not only UIImage but also strings, JSON, Data, and entire Core Data stores. This makes it a versatile tool not only for UI snapshots but also for verifying serialization and decoding of JSON responses. For SwiftUI, use the assertSnapshot extension with the .image(on: .iPhone13) modifier. The record: true strategy creates references on first run.
For React Native, the popular solution is react-native-testing-library combined with jest-image-snapshot. The web approach to snapshot testing is ported to the mobile environment by rendering components in a Node.js environment followed by comparison of JSON snapshots of the virtual DOM. This approach is faster than native but less accurate — it does not account for platform-specific font rendering and system component features. For Flutter, golden testing is used through the goldens toolkit.
Let’s look at snapshot tests for Android (Paparazzi) and iOS (SnapshotTesting). Both examples verify the appearance of a component — a user card with an avatar, name, and status. The test renders the component with test data and compares the result against a reference image stored in the repository.
Paparazzi uses the @Test annotation and snapshot() method to capture the render. References are saved in the src/test/snapshots folder and automatically loaded on the next run for comparison.
class UserCardSnapshotTest {
@get:Rule
val paparazzi = Paparazzi(
theme = "Theme.MyApp",
maxPercentDifference = 0.1
)
@Test
fun userCard_defaultState() {
val card = UserCard(
name = "Alice Johnson",
status = "Online",
avatarUrl = "https://example.com/avatar.png"
)
paparazzi.snapshot(card)
}
@Test
fun userCard_offlineState() {
val card = UserCard(
name = "Bob Smith",
status = "Offline",
avatarUrl = null
)
paparazzi.snapshot(card, name = "user_card_offline")
}
}
SnapshotTesting uses the .snapshot() modifier inside assertSnapshot. The library automatically determines the format — UIImage for UIView, String for text, Data for binary data.
import SnapshotTesting
import XCTest
class UserCardSnapshotTests: XCTestCase {
func testUserCardDefaultState() {
let card = UserCardView(
name: "Alice Johnson",
status: "Online",
avatarURL: URL(string: "https://example.com/avatar.png")
)
let controller = UIHostingController(rootView: card)
assertSnapshot(matching: controller, as: .image(on: .iPhone13))
}
func testUserCardOfflineState() {
let card = UserCardView(
name: "Bob Smith",
status: "Offline",
avatarURL: nil
)
assertSnapshot(matching: card, as: .image(on: .iPhone13))
}
}
A typical workflow includes four stages. First run (record mode): all snapshot tests execute in record mode — reference images are created and saved to the repository. This stage is performed during initial test setup or after an intentional interface change. After recording, references are committed together with the code — they become part of the project.
On subsequent runs, tests work in comparison mode: each new render is compared against the reference. If differences are found, a diff image is generated: pixels matching the reference are highlighted in green, differing ones in red. The developer reviews the diff and makes a decision: if the change is expected (conscious design change), the reference is updated with the record command; if unexpected, the bug is fixed. Reference updates are performed with a single command: for Paparazzi it’s `./gradlew recordPaparazzi`, for SnapshotTesting — `assertSnapshot(record: true)`.
According to the Spotify Engineering Blog (2022), teams using the described workflow spend an average of 2 minutes per test analyzing diff images. With a set of 50 snapshot tests, a full reference update cycle takes 15–20 minutes, significantly faster than manual verification of visual changes across 50 screens.
Snapshot tests have fundamental limitations. Environment sensitivity: the same component may render differently across different OS versions, screen densities, and font configurations. References created on one machine may differ from renders on a CI server. The solution is to use fixed environment parameters: a specific Layoutlib version for Paparazzi or an exact device model for SnapshotTesting.
Anti-pattern #1: giant snapshots — a snapshot test capturing the entire screen fails with every minimal change to any component. The correct approach is to test individual components (button, card, input field) in isolation. Each component is tested independently, providing precise identification of the change source. Anti-pattern #2: ignoring diffs — automatically updating references without analyzing diff images reduces the value of snapshot tests to zero. Each diff requires a conscious developer decision.
According to the Better Engineering Blog (2023), snapshot tests provide the greatest value when covering design system components and key screens in basic states — empty, filled, error, and boundary. Covering animations and dynamic states through snapshot tests is inefficient due to the non-deterministic nature of timestamps in rendering — for such scenarios, video recording or manual QA is more suitable.
Frequently Asked Questions
No, snapshot tests check visual appearance, while UI tests check interface behavior. The optimal strategy is to combine both approaches: snapshots for visual regression, UI tests for scenarios and navigation. Snapshots answer “does it look right”, UI tests answer “does it work right”.
References are updated with every conscious design change: a new theme color, modified margins, adding or removing elements. Updating is done through record mode, after which diff images are reviewed in code review to ensure the changes match designer expectations.
First and foremost, design system components — buttons, cards, input fields, modal windows. Then key screens in basic states. Do not test with snapshots animations, WebView, maps, and screens with dynamic content — snapshots produce false failures for these due to non-determinism.
Use the same API Level for both record and test modes. For Paparazzi, specify a specific Layoutlib version in the configuration. For SnapshotTesting, fix the device model. References created on Android 14 may differ from renders on Android 12 due to changes in system fonts and Material theme.
In CI, snapshot tests run in verify mode. If a test fails, CI shows the diff image in build artifacts. Record mode (reference update) is performed locally by the developer or in a separate CI task with manual trigger. Reference images must be committed to the repository.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also