Delightful Swift snapshot testing.
This is a fork of pointfreeco/swift-snapshot-testing. It aims to integrate pull requests and issues more proactively, while making long-overdue changes like Swift 6 support.
Once installed, no additional configuration is required. You can import the
SnapshotTesting module and call the assertSnapshot function.
import SnapshotTesting
import Testing
import UIKit
@MainActor
struct MyViewControllerTests {
@Test func myViewController() {
let vc = MyViewController()
assertSnapshot(of: vc, as: .image)
}
}When an assertion first runs, a snapshot is automatically recorded to disk and the test will fail, printing out the file path of any newly-recorded reference.
❌ failed - No reference was found on disk. Automatically recorded snapshot: …
open "…/MyAppTests/__Snapshots__/MyViewControllerTests/testMyViewController.png"
Re-run "testMyViewController" to test against the newly-recorded snapshot.
Repeat test runs will load this reference and compare it with the runtime value. If they don't match, the test will fail and describe the difference. Failures can be inspected from Xcode's Report Navigator or by inspecting the file URLs of the failure.
You can record a new reference by customizing snapshots inline with the assertion, or using the
withSnapshotTesting tool:
// Record just this one snapshot
assertSnapshot(
of: vc,
as: .image,
options: SnapshotAssertionOptions().recording(.all)
)
// Record all snapshots in a scope:
withSnapshotTesting(record: .all) {
assertSnapshot(of: vc1, as: .image)
assertSnapshot(of: vc2, as: .image)
assertSnapshot(of: vc3, as: .image)
}
// Record all snapshot failures in a Swift Testing suite:
@Suite(.snapshots(record: .failed))
struct FeatureTests {}
// Record all snapshot failures in an 'XCTestCase' subclass:
class FeatureTests: XCTestCase {
override func invokeTest() {
withSnapshotTesting(record: .failed) {
super.invokeTest()
}
}
}SwiftUI image snapshots use en_US_POSIX, UTC, and the Gregorian calendar by default. Override
them for a scope when needed:
let configuration = SnapshotTestingConfiguration()
.usingLocale(Locale(identifier: "fr_CA"))
.usingTimeZone(TimeZone(identifier: "America/Toronto"))
.usingCalendar(Calendar(identifier: .gregorian))
withSnapshotTesting(configuration) {
assertSnapshot(of: view, as: .image)
}References can also be centralized beneath a test target while preserving the source-file hierarchy:
let configuration = SnapshotTestingConfiguration(
referenceStorage: .directory("__Snapshots__", relativeTo: .testTarget)
)
withSnapshotTesting(configuration) {
assertSnapshot(of: view, as: .image)
}This opt-in policy places a test in Flows/ChatViewTests.swift under
__Snapshots__/Flows/ChatViewTests/. Move existing references to the new hierarchy before enabling
the policy, and exclude only the centralized __Snapshots__ directory from the SwiftPM test
target. The test-target directory must match the test module name; use
SnapshotAssertionOptions.savingSnapshots(in:) for custom source layouts.
Image comparison precision can be reused independently from assertion and environment options:
let imageOptions = ImageSnapshotOptions()
.requiringPixelPrecision(0.99)
.requiringPerceptualPrecision(0.98)
assertSnapshot(of: view, as: .image(options: imageOptions))Add the lightweight SnapshotPreviews product to the target that owns your SwiftUI views. A
concrete preview type must spell both conformances so that Xcode can discover PreviewProvider:
import SnapshotPreviews
import SwiftUI
struct MyView_Previews: PreviewProvider, SnapshotProvider {
static let defaultLayout: PreviewSnapshotLayout = .device
@SnapshotBuilder
static var snapshots: [PreviewSnapshot] {
PreviewSnapshot("Default") {
MyView()
}
PreviewSnapshot("Compact", layout: .fixed(width: 320, height: 480)) {
MyView()
}
}
}SnapshotProvider supplies the previews implementation. The same named views can then be image
snapshot-tested from a test target that imports SnapshotTesting:
@testable import MyFeature
import SnapshotTesting
import Testing
@MainActor
struct MyViewTests {
@Test func snapshots() {
let compactLight = ViewImageConfig.iPhone17Pro
var compactDark = compactLight
compactDark.traits = UITraitCollection(userInterfaceStyle: .dark)
assertSnapshots(
of: MyView_Previews.self,
configurations: [
NamedViewImageConfig(name: "compact-light", device: compactLight),
NamedViewImageConfig(name: "compact-dark", device: compactDark)
]
)
}
}PreviewSnapshot uses the provider's defaultLayout when no layout is supplied; providers default
to .sizeThatFits. Use resolvedSnapshots when a custom assertion loop needs the effective
layouts. A .device snapshot requires the single on: ViewImageConfig overload or a named
configuration matrix on iOS and tvOS. Matrix references use stable names such as
snapshots.compact-light.Default.png, and each device configuration's traits apply automatically.
On macOS and watchOS, image snapshot assertions support fixed and size-to-fit layouts.
Transform a snapshot or collection without reconstructing its metadata:
let snapshots = rawSnapshots.transformingViews {
$0.injectingPreviewEnvironment()
}Snapshot names must be unique within a provider because they name the reference files. The public
raw snapshots and resolved resolvedSnapshots collections remain available for custom strategies
or rendering behavior.
While most snapshot testing libraries in the Swift community are limited to UIImages of UIViews,
SnapshotTesting can work with any format of any value on any Swift platform!
The assertSnapshot function accepts a value and any snapshot strategy that value supports. This
means that a view or view controller can be tested against an image representation and against a
textual representation of its properties and subview hierarchy.
assertSnapshot(of: vc, as: .image)
assertSnapshot(of: vc, as: .recursiveDescription)View testing is highly configurable. You can override trait collections (for specific size classes and content size categories) and generate device-agnostic snapshots, all from a single simulator.
assertSnapshot(of: vc, as: .image(on: .iPhoneSe))
assertSnapshot(of: vc, as: .recursiveDescription(on: .iPhoneSe))
assertSnapshot(of: vc, as: .image(on: .iPhoneSe(.landscape)))
assertSnapshot(of: vc, as: .recursiveDescription(on: .iPhoneSe(.landscape)))
assertSnapshot(of: vc, as: .image(on: .iPhoneX))
assertSnapshot(of: vc, as: .recursiveDescription(on: .iPhoneX))
assertSnapshot(of: vc, as: .image(on: .iPadMini(.portrait)))
assertSnapshot(of: vc, as: .recursiveDescription(on: .iPadMini(.portrait)))Warning Snapshots must be compared using the exact same simulator that originally took the reference to avoid discrepancies between images.
SnapshotTesting does not enforce a global capture environment because a test suite may intentionally
run references on multiple simulators. The simulator running the test is independent of the
ViewImageConfig passed to .image(on:). If a suite requires a fixed simulator, add a client-side
preflight:
import Foundation
import SnapshotTesting
import Testing
func assertSnapshotEnvironment(
simulatorModelIdentifier: String,
iOSMajorVersion: Int
) -> Bool {
let processInfo = ProcessInfo.processInfo
let actualModel = processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"]
let actualVersion = processInfo.operatingSystemVersion.majorVersion
guard actualModel == simulatorModelIdentifier, actualVersion == iOSMajorVersion else {
Issue.record(
Comment(
rawValue: "Expected \(simulatorModelIdentifier) on iOS \(iOSMajorVersion); "
+ "used \(actualModel ?? "a non-simulator device") on iOS \(actualVersion)."
)
)
return false
}
return true
}
@MainActor @Test func testFeature() {
guard assertSnapshotEnvironment(
simulatorModelIdentifier: "iPhone15,4",
iOSMajorVersion: 17
) else { return }
let vc = MyViewController()
assertSnapshot(of: vc, as: .image)
}Keep the expected values with the tests that need them. This check is intentionally opt-in and can be extended by clients that also need to validate display scale or color gamut.
Better yet, SnapshotTesting isn't limited to views and view controllers! There are a number of available snapshot strategies to choose from.
For example, you can snapshot test URL requests (e.g., those that your API client prepares).
assertSnapshot(of: urlRequest, as: .raw)
// POST http://localhost:8080/account
// Cookie: pf_session={"userId":"1"}
//
// email=blob%40pointfree.co&name=BlobAnd you can snapshot test Encodable values against their JSON and property list representations.
assertSnapshot(of: user, as: .json)
// {
// "bio" : "Blobbed around the world.",
// "id" : 1,
// "name" : "Blobby"
// }
assertSnapshot(of: user, as: .plist)
// <?xml version="1.0" encoding="UTF-8"?>
// <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
// "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
// <plist version="1.0">
// <dict>
// <key>bio</key>
// <string>Blobbed around the world.</string>
// <key>id</key>
// <integer>1</integer>
// <key>name</key>
// <string>Blobby</string>
// </dict>
// </plist>In fact, any value can be snapshot-tested using SnapshotTestingCustomDump!
import SnapshotTestingCustomDump
assertSnapshot(of: user, as: .customDump)
// User(
// bio: "Blobbed around the world.",
// id: 1,
// name: "Blobby"
// )If your data can be represented as an image, text, or data, you can write a snapshot test for it!
Read the documentation site for guides, examples, release notes, and API documentation for SnapshotTesting, SnapshotPreviews, InlineSnapshotTesting, and CustomDump.
Swift 6.3. Apple platforms require iOS 17, macOS 13, tvOS 17, or watchOS 10 or later. CI also tests Linux and Android.
On watchOS, image snapshots support UIImage values and SwiftUI views rendered by ImageRenderer.
Pixel precision is supported, but perceptual precision is not. ImageRenderer may use placeholders
for views backed by native platform frameworks.
On macOS 13 and later, SwiftUI views support .image snapshots rendered at a fixed 2x scale.
The default sizeThatFits layout uses the view's ideal size, while fixed centers it in the
requested point size. Compare snapshots on the same OS version to avoid system-rendering differences.
Warning By default, Xcode will try to add the SnapshotTesting package to your project's main application/framework target. Please ensure that SnapshotTesting is added to a test target instead, as documented in the last step, below.
- From the File menu, select Add Package Dependencies….
- Enter package repository URL:
https://github.com/modern-swift-dev/swift-snapshot-testing. - Confirm the version and let Xcode resolve the package.
- On the final dialog, update SnapshotTesting's Add to Target column to a test target that will contain snapshot tests (if you have more than one test target, you can later add SnapshotTesting to them by manually linking the library in its build phase).
If you reuse SwiftUI previews, add the package's SnapshotPreviews product to the application or
framework target that owns those views. Keep the full SnapshotTesting product linked only to test
targets.
If you want to use SnapshotTesting in any other project that uses
SwiftPM, add the package as a dependency in Package.swift:
dependencies: [
.package(
url: "https://github.com/modern-swift-dev/swift-snapshot-testing",
branch: "main"
),
]Next, add SnapshotTesting as a dependency of your test target:
targets: [
.target(name: "MyApp"),
.testTarget(
name: "MyAppTests",
dependencies: [
"MyApp",
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
]
)
]To share SwiftUI preview definitions with snapshot tests, add the lightweight SnapshotPreviews
product to the source target as well:
targets: [
.target(
name: "MyApp",
dependencies: [
.product(name: "SnapshotPreviews", package: "swift-snapshot-testing"),
]
),
.testTarget(
name: "MyAppTests",
dependencies: [
"MyApp",
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
]
),
]The package also includes InlineSnapshotTesting. Import it and use assertInlineSnapshot to
store string snapshots directly in test source files.
The package includes CustomDump, a collection of tools for dumping, diffing, and testing Swift
values. Add the CustomDump product to use its APIs directly.
For snapshot strategies, add the SnapshotTestingCustomDump product and import it to use
.customDump with snapshot or inline snapshot assertions.
Custom Dump was imported from
pointfreeco/swift-custom-dump at commit
924df7d.
- Dozens of snapshot strategies. Snapshot
testing isn't just for
UIViews andCALayers. Write snapshots against any value. - Write your own snapshot strategies. If you can convert it to an image, string, data, or your own diffable format, you can snapshot test it! Build your own snapshot strategies from scratch or transform existing ones.
- No configuration required. Don't fuss with scheme settings and environment variables. Snapshots are automatically saved alongside your tests.
- Flexible recording. Missing snapshots are recorded by default. Use
withSnapshotTestingwith.all,.failed, or.neverto choose another recording mode. - Subclass-free. Assert from any XCTest case or Quick spec.
- Device-agnostic snapshots. Render views and view controllers for specific devices and trait collections from a single simulator.
- First-class Xcode support. Image differences are captured as XCTest attachments. Text differences are rendered in inline error messages.
- Modern platform support. Supports iOS 17+, macOS 13+, tvOS 17+, watchOS 10+, Linux, and Android.
- SceneKit, SpriteKit, and WebKit support. Most snapshot testing libraries don't support these view subclasses.
Codablesupport. Snapshot encodable data structures into their JSON and property list representations.- Custom diff tool integration. Configure failure messages to print diff commands for
Kaleidoscope or your diff tool of choice.
withSnapshotTesting(diffTool: .ksdiff) { assertSnapshot(of: vc, as: .image) }
See CONTRIBUTING.md for contributor instructions.
This library is released under the MIT license. See LICENSE and LICENSE-swift-custom-dump for details.