Snapshot Testing in Swift with swift-snapshot-testing
Snapshot testing captures a reference output the first time a test runs, then compares every subsequent run against that reference. Any visual or structural drift becomes a failing test—no manual inspection required. PointFree's swift-snapshot-testing library is the de-facto standard for this in Swift.
Installation
Add the package in Xcode via File > Add Package Dependencies:
https://github.com/pointfreeco/swift-snapshot-testingOr in Package.swift:
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.0")Add SnapshotTesting to your test target's dependencies.
Your First Snapshot Test
import XCTest
import SnapshotTesting
@testable import MyApp
final class ProfileCardTests: XCTestCase {
func testProfileCardDefaultState() {
let card = ProfileCardView(
name: "Alice Johnson",
role: "iOS Engineer",
avatarURL: nil
)
assertSnapshot(of: card, as: .image(on: .iPhone13))
}
}Run the test the first time with record: true (or with the RECORD_SNAPSHOTS env var) to save the reference image:
assertSnapshot(of: card, as: .image(on: .iPhone13), record: true)The library writes a PNG to __Snapshots__/<TestClassName>/testProfileCardDefaultState.1.png next to your test file. Commit this file to source control—it is the ground truth.
On subsequent runs, omit record: true. The library renders the view fresh and diffs it pixel-by-pixel against the stored PNG. Any difference fails the test and writes a failure image showing the diff.
Snapshot Strategies
assertSnapshot accepts a Snapshotting strategy, which determines what is captured and how it is serialized.
UIView / UIViewController
// Render at a specific device size
assertSnapshot(of: viewController, as: .image(on: .iPadPro12_9))
// Render in light and dark mode
assertSnapshot(of: view, as: .image(traits: .init(userInterfaceStyle: .dark)))SwiftUI Views
import SwiftUI
func testBannerView() {
let banner = BannerView(message: "Update available", style: .info)
assertSnapshot(of: banner, as: .image(layout: .fixed(width: 375, height: 80)))
}Plain Text / String Output
Snapshot strategies aren't limited to images. You can snapshot any Encodable value as JSON, or any string:
func testErrorMessageFormatting() {
let formatter = ErrorFormatter()
let message = formatter.format(.networkUnavailable)
assertSnapshot(of: message, as: .lines)
}Data Structures as Dump Output
func testCartContents() {
let cart = Cart(items: [
CartItem(sku: "BOOT-42", quantity: 2, price: 89.99),
CartItem(sku: "LACE-01", quantity: 1, price: 4.99),
])
assertSnapshot(of: cart, as: .dump)
}The .dump strategy uses Swift's dump() function, producing a textual tree. This is excellent for model objects where you care about structure, not pixels.
Inline Snapshots
For string-based outputs you can embed the snapshot directly in the test source, avoiding a separate file:
func testJSONEncoding() {
let order = Order(id: "ord-001", total: 49.99, currency: "USD")
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let json = String(data: try! encoder.encode(order), encoding: .utf8)!
assertInlineSnapshot(of: json, as: .lines) {
"""
{
"currency" : "USD",
"id" : "ord-001",
"total" : 49.99
}
"""
}
}On first run, the library fills in the trailing closure content. On subsequent runs it diffs the current output against the string. Great for API response shapes or formatted output.
Re-Recording Snapshots
When you intentionally change a UI component, existing snapshots become stale. Re-record with the RECORD_SNAPSHOTS=1 environment variable in the scheme:
- Edit scheme → Test → Environment Variables → add
RECORD_SNAPSHOTS = 1. - Run the tests. New PNGs overwrite the old ones.
- Remove the env var.
- Commit the updated snapshots alongside the code change.
Alternatively, call assertSnapshot(..., record: true) temporarily in specific tests, then remove the flag before committing.
Handling Dynamic Content
Dates, random identifiers, and live network images make snapshots flaky. Inject deterministic values instead:
func testReceiptView() {
let receipt = ReceiptView(
orderID: "ORD-12345", // fixed, not UUID()
date: Date(timeIntervalSince1970: 1_700_000_000), // fixed
total: "$123.45"
)
assertSnapshot(of: receipt, as: .image(on: .iPhone13))
}For remote images, inject a synchronous local UIImage through a test double rather than loading from the network.
CI Integration
Snapshot tests require a consistent rendering environment. Run them on a fixed simulator to avoid OS-version font-metric differences:
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyAppTests \
-destination "platform=iOS Simulator,name=iPhone 15,OS=17.4" \
-resultBundlePath TestResults.xcresultSet SNAPSHOT_ARTIFACTS to a known path to find failure diffs in CI artifacts:
SNAPSHOT_ARTIFACTS=/tmp/snapshot-failures xcodebuild test ...Upload /tmp/snapshot-failures as a CI artifact so you can inspect diffs in pull requests.
Accessibility Snapshot Tests
The library ships with an accessibility snapshot strategy that captures the accessibility tree as text—useful for regression-testing VoiceOver content:
func testProfileCardAccessibility() {
let card = ProfileCardView(name: "Alice", role: "Engineer", avatarURL: nil)
assertSnapshot(of: card, as: .accessibilityElements(on: .iPhone13))
}Key Takeaways
- Record snapshots once, commit them, and the library guards against regressions automatically.
- Use
.imagefor UI,.dumpfor models,.linesfor formatted strings, andassertInlineSnapshotfor short outputs you want readable in the test file. - Fix dynamic content by injecting fixed dates, IDs, and local images.
- Pin your CI simulator and OS version to keep snapshots stable across machines.
Snapshot tests catch regressions the moment they're introduced. For continuous monitoring beyond your CI pipeline, HelpMeTest runs your tests on a schedule and alerts you when something breaks in production.