Snapshot Testing in Swift with swift-snapshot-testing

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-testing

Or 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:

  1. Edit scheme → Test → Environment Variables → add RECORD_SNAPSHOTS = 1.
  2. Run the tests. New PNGs overwrite the old ones.
  3. Remove the env var.
  4. 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.xcresult

Set 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 .image for UI, .dump for models, .lines for formatted strings, and assertInlineSnapshot for 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.

Read more

Start now free