How do snapshot tests work, and what are their pitfalls: rendering differences, dynamic content, size, and review fatigue?

22.5.1 · L3 · Interview

Predict first

A label is rendered to PNG data twice with the same text and once with one character changed. What do the comparisons report?

import UIKit

@MainActor func render(_ text: String) -> Data {
    let label = UILabel(frame: CGRect(x: 0, y: 0, width: 120, height: 40))
    label.text = text
    label.textColor = .black
    label.backgroundColor = .white
    let format = UIGraphicsImageRendererFormat()
    format.scale = 2
    let renderer = UIGraphicsImageRenderer(size: label.bounds.size, format: format)
    return renderer.pngData { ctx in label.layer.render(in: ctx.cgContext) }
}

@MainActor func run() {
    let reference = render("Hello")
    print("identical render matches:", render("Hello") == reference)
    print("changed text differs:", render("Hellp") != reference)
}
MainActor.assumeIsolated { run() }
  • identical render matches: true, changed text differs: true
  • identical render matches: false, because rendering is random
  • changed text differs: false

Predict first

A text snapshot of a value uses dump. Does the dump of a small struct equal the expected block?

struct Receipt { var title: String; var lines: [Line]; var total: Double }
struct Line { var name: String; var qty: Int }

let receipt = Receipt(title: "Order", lines: [Line(name: "Tea", qty: 2)], total: 4.5)
var out = ""
dump(receipt, to: &out)
let module = String(reflecting: Receipt.self).dropLast("Receipt".count)
let expected = """
▿ \(module)Receipt
  - title: "Order"
  ▿ lines: 1 element
    ▿ \(module)Line
      - name: "Tea"
      - qty: 2
  - total: 4.5

"""
print(out == expected)
  • Yes, it matches exactly
  • No, dump output changes every run

Mental model. A snapshot test is a spot-the-difference puzzle: great for noticing that something moved, useless for telling you whether the new picture is the one you wanted.

A snapshot test renders a view (or a value) and compares the result with a stored reference; the first run records it, later runs fail on any difference. Rendering a view to an image is deterministic for the same input on the same machine (the check below renders twice and gets identical bytes, and a one-character text change differs), which is what makes the idea work. The popular open-source swift-snapshot-testing package also snapshots values as text: the dump of a struct, a view hierarchy description or an encoded JSON, which is cheaper, diffs cleanly in code review, and is often the better choice for non-visual data. Snapshots are excellent for catching accidental visual changes across states and configurations (light and dark, several Dynamic Type sizes, right-to-left, small and large devices) in a fast unit-test process, without launching the full app.

The pitfalls are about reliability and review. Image output can differ between operating-system versions, simulator devices, scales, GPUs and font rendering, so a reference recorded on one machine fails on CI; pin the simulator model and OS for snapshot runs, record on the same setup, and use a small precision tolerance rather than exact match when needed. Anything dynamic breaks it: current date, network images, animations, random data, the keyboard; stub them to fixed values and disable animation. Keep snapshots small and focused (a component per state, not whole screens with unrelated content), or one change touches hundreds of images. Review image diffs deliberately, because a huge PR with regenerated references is rubber-stamped and then the tests protect nothing. Store references in the repository (large images may need Git LFS) and make “record mode” explicit, never automatic. Snapshots tell you that something changed, not that it is right.

Rendering the same view on the same machine is deterministic, and any pixel change shows up as a difference.

The dump of a value is stable text, which makes it a readable and reliable snapshot for data.

Common traps

  • Recording references on a laptop and comparing on CI with a different OS or device. Everything fails on pixel differences that are not regressions.
  • Regenerating all references to make the suite green. A wrong change is now the new baseline.

They might follow up with

  • When is a text snapshot better? For data and structure: it is stable, small and readable in a diff.
  • How do you keep snapshots stable on CI? A fixed simulator model and OS, stubbed dynamic data, animations off, and the same scale.

Related questions

  • 22.6.1 How do you test SwiftUI: view models, hosting a view, ImageRenderer snapshots and ViewInspector?
  • 22.5.2 How do you write UI tests that stay stable: accessibility identifiers, page objects, launch arguments…
  • 22.1.1 What is the test pyramid for an iOS app, and what does each layer cost and catch?
  • 9.2.1 Walk through how a frame gets from your code to the screen.

How do you write UI tests that stay stable: accessibility… →