Developing Snapshot Tests for Mobile Applications
Snapshot testing is automatic control that UI hasn't changed unintentionally. Developer fixes padding in one component, forgets about settings screen where the same component is used with different props — and after a week in production broken layout appears. Snapshot test catches this in seconds.
Simple: on first run test saves reference image or serialized component tree. On each subsequent run compares to reference. Discrepancy = failing test.
iOS: iOSSnapshotTestCase
On iOS snapshot tests built on FBSnapshotTestCase (Facebook, moved to pointfreeco/swift-snapshot-testing) or native XCTAttachment + manual comparison. Most mature solution — pointfreeco/swift-snapshot-testing:
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 __Snapshots__/ProfileViewControllerTests/testProfileScreen.1.png. On subsequent runs compares pixel by pixel. Threshold — 0, any difference = fail.
Supported snapshot strategies: .image (screenshot), .recursiveDescription (text view tree), .dump (structure). For component libraries .image is main.
Font Problem in CI
Text rendering on simulator may differ from CI due to different system font versions. Solution — pin simulator (iPhone 15, iOS 17.4) and use same Xcode version. In fastlane through xcversion:
xcversion(version: "~> 15.3")
Android: Paparazzi
For Android best tool — Paparazzi by Square. Doesn't require emulator: renders View through LayoutInflater in JVM environment, using layoutlib (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 = "Save",
onClick = {}
)
}
}
}
Execution: ./gradlew recordPaparazziDebug (record baselines) and ./gradlew verifyPaparazziDebug (verify).
Paparazzi supports Jetpack Compose with paparazzi.snapshot { ComposableFunction() } — same mechanics.
Main advantage over emulator screenshot tests: speed. Paparazzi test runs in 200–500 ms vs 5–10 seconds on emulator. For 200-component library — 30 minutes vs 3 minutes difference.
Flutter: Golden Tests
In Flutter snapshot tests called Golden Tests and 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 baselines: flutter test --update-goldens.
Platform dependency of golden files — known issue. macOS renders font differently than Linux (CI). Solution: keep golden files generated on CI (Linux), locally developers use flutter test --update-goldens only on same OS. Or golden_toolkit package with loadAppFonts() which neutralizes some differences.
Managing Baselines in Git
Reference snapshots stored in repo. Several rules:
-
__Snapshots__/,src/test/snapshots/,test/goldens/— add to.gitattributesas binary:*.png binary - PR changing UI should include updated snapshots:
git add test/goldens/ && git commit -m "update snapshots" - In CI run only verification (
verify), not recording (record). Recording only locally or through special workflow
If CI fails due to snapshot difference — not test error, it's signal of unplanned UI change. Good.
Timeline
2–3 days — tool setup + snapshot tests for key components. Full component library coverage (50+ components) — estimate separately. Cost is calculated individually.







