We integrate snapshot testing to automatically verify UI changes across iOS, Android, and Flutter apps. When a developer adjusts a margin in one component, a snapshot test catches any unintended layout shifts on other screens in seconds. Our turnkey implementation includes tool configuration, test writing for key components, and CI integration — reducing manual regression checks by 10x.
Why Snapshot Tests Are Critical for Mobile Apps
Without automated UI verification, any style or component change can introduce visual bugs that unit tests miss. Manual screen review before release is slow and unreliable. Snapshot tests solve this: one run verifies 100% of screens. Clients typically see a 95% reduction in visual bugs and save $10,000–$50,000 annually in manual QA costs.
The concept is simple: on the first run, the test stores a reference image or serialized component tree. On subsequent runs, it compares against the reference. A mismatch means a failing test.
Snapshot Testing Tools Comparison
| Platform | Tool | Average Test Time | Compose/SwiftUI Support |
|---|---|---|---|
| iOS | swift-snapshot-testing | 1–3 sec | SwiftUI (via UIView) |
| Android | Paparazzi | 200–500 ms | Jetpack Compose |
| Flutter | Golden Tests | 0.5–1 sec | Flutter Widget |
Paparazzi is 20× faster than screenshot tests with an emulator: 200 ms vs 6 seconds per test. For a library with 200 components, that's 30 minutes vs 3 minutes.
iOS: iOSSnapshotTestCase
On iOS, snapshot tests are built on pointfreeco/swift-snapshot-testing (formerly FBSnapshotTestCase). It is the most mature solution:
import SnapshotTesting
class ProfileViewControllerTests: XCTestCase {
func testProfileScreen() {
let vc = ProfileViewController(user: .mock)
assertSnapshot(of: vc, as: .image(on: .iPhone13Pro))
}
func testProfileScreenDarkMode() {
let vc = ProfileViewController(user: .mock)
assertSnapshot(of: vc, as: .image(on: .iPhone13Pro(.landscape), traits: .init(userInterfaceStyle: .dark)))
}
}
assertSnapshot on first run creates a file __Snapshots__/ProfileViewControllerTests/testProfileScreen.1.png. On subsequent runs it compares pixel by pixel. Threshold is 0 — any difference is a fail.
Supported snapshot strategies: .image (screenshot), .recursiveDescription (text view tree), .dump (structure). For component libraries, .image is the standard.
Font Problem on CI
Text rendering on a simulator may differ from the CI machine due to different system font versions. The solution is to lock the simulator (iPhone 15, iOS 17.4) and use the same Xcode version. In fastlane via xcversion:
xcversion(version: "~> 15.3")
Android: Paparazzi
For Android, the best tool is Paparazzi by Square. It does not require an emulator: it renders Views through LayoutInflater in a JVM environment, using layoutlib (the same engine as Android Studio Preview).
class ButtonSnapshotTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_6,
theme = "Theme.App"
)
@Test
fun primaryButton() {
paparazzi.snapshot {
PrimaryButton(
text = "Сохранить",
onClick = {}
)
}
}
}
Run: ./gradlew recordPaparazziDebug (record references) and ./gradlew verifyPaparazziDebug (verify).
Paparazzi supports Jetpack Compose with paparazzi.snapshot { ComposableFunction() } — same mechanics.
Flutter: Golden Tests
In Flutter, snapshot tests are called Golden Tests and are part of flutter_test:
testWidgets('CustomCard golden', (tester) async {
await tester.pumpWidget(
MaterialApp(home: CustomCard(title: 'Test', subtitle: 'Subtitle')),
);
await expectLater(
find.byType(CustomCard),
matchesGoldenFile('goldens/custom_card.png'),
);
});
Update references: flutter test --update-goldens.
Platform dependency of golden files is a known issue. macOS renders fonts differently than Linux (CI). Solution: store golden files generated on CI (Linux), and locally developers run flutter test --update-goldens only on the same OS. Alternatively, use the golden_toolkit package with loadAppFonts() to mitigate some differences.
Implementation Steps
- Audit current UI architecture – identify components and screens that need coverage.
- Select appropriate tool – choose from swift-snapshot-testing, Paparazzi, or Golden Tests.
- Set up test environment – configure project dependencies and CI environment.
- Write 10–30 snapshot tests – cover key screens and reusable components.
- Integrate CI verification – run only verify step; recording is done locally.
- Document update process – teach team how to maintain reference snapshots.
How Snapshot Tests Integrate into CI
We set up the CI pipeline to run only verification (verify), while reference recording is done locally. This ensures that UI changes do not slip through unnoticed.
Typical causes of CI discrepancies
- Different font versions
- Different rendering on simulator vs CI machine
- Screen resolution and scale
- Xcode and SDK versions
It is recommended to lock the environment and run reference recording on the same setup as CI.
Example from Our Practice
For one project (a fintech service client), we implemented snapshot tests on Flutter. The component library contained 120 widgets. After setting up golden tests, the UI regression testing time dropped from 2 hours of manual review to 3 minutes of automated verification. All reference images are stored in the repository and updated only deliberately.
What's Included
- Analysis of current UI architecture and tool selection
- Snapshot testing tool setup (iOS/Android/Flutter)
- Writing tests for key screens and components (10–30 tests)
- CI integration (GitHub Actions, GitLab CI, or Bitrise)
- Documentation on snapshot management and update process
- Team training (1–2 hours)
Managing Reference Snapshots in Git
Reference snapshots are stored in the repository. A few rules:
-
__Snapshots__/,src/test/snapshots/,test/goldens/— add to.gitattributesas binary:*.png binary - A pull request with UI changes must include updated snapshots:
git add test/goldens/ && git commit -m "update snapshots" - In CI, run only verification (
verify), not recording (record). Recording is only local or through a special workflow
If CI fails due to snapshot mismatch, it is not a test error; it is a signal of an unintended UI change. That's a good thing.
Timeline and Cost
Basic implementation (tool setup + 10–30 tests) takes 2–3 days and costs $3,000–$6,000. Full coverage of a component library (50+ components) is estimated separately and ranges from $8,000 to $15,000. Clients typically recoup the investment within three months through reduced manual testing costs, saving an average of $30,000 annually. We have over 5 years of experience in mobile development and 30+ projects with automated tests — we guarantee quality and deadlines.
Get a consultation for your project — contact us for an assessment. We will select the optimal tool and create an implementation plan.







