• Swift 6
  • macOS · iOS · tvOS · watchOS
  • swift-syntax + swift-format
  • MIT

Test fixtures
the compiler checks.

Capture a value your app actually produced. SwiftLiteral writes it out as Swift source that rebuilds it. Commit the file. Rename a property and the fixture stops compiling, at the line.

OrderTests.swift
let order = try JSONDecoder().decode(Order.self, from: response)

try Literal.write(order, named: "shippedOrder")
__Snapshots__/Order+shippedOrder.swift
import Foundation

extension Order {
    static let shippedOrder: Order = Order(
        id: UUID(uuidString: "8C6D4E2A-0000-4000-8000-1B2C3D4E5F60")!,
        placedAt: Date(timeIntervalSince1970: 1710000000.0),
        status: .shipped,
        lines: [Order.Line(sku: "WDG-001", quantity: 2, unitPrice: Decimal(string: "29.99")!)]
    )
}

Why Swift, not JSON

A JSON fixture goes stale in silence.
A Swift one stops building.

Rename name to fullName. The JSON keeps the old key and nothing complains until a decode fails at runtime, in a test, with a message about a missing key. The Swift fixture does not compile, and Xcode points at the line.

user.json
{ "name": "Alice" }

Still loads. Fails later, in some test, as a decoding error.

User+testUser.swift
User(name: "Alice")  // error: incorrect argument label in call (have 'name:', expected 'fullName:')

Does not build. The compiler names the file and the line.

It also beats a fixture typed by hand. A hand-written fixture is someone's guess at what the decoder produces. This one is the value your app made, every field of it, including the optional that is present and the date in the format the backend changed in March.

Features

Output that compiles, or an error that says why.

The whole point is that the generated file builds. When a value cannot be written as Swift that rebuilds it, SwiftLiteral throws with the type and the path to it, rather than writing a file that looks right and is not.

Typechecked in CI

The test suite renders a matrix of samples, writes them beside the real types and runs swiftc -typecheck over both. Parsing is not enough: User(name: nil) parses.

Your types, as they are

Structs, classes with their superclass state, enums with associated values, generics, tuples, nested types by their full path (Order.Line), and optionals including .some(nil).

Foundation built in

Date, UUID, URL, Data, Decimal, Locale and TimeZone. Double round-trips exactly. Dictionary keys keep their type: [1: "one"], not ["1": "one"].

Deterministic

Two runs over equal values give the same bytes. Dictionary keys and set elements sort, so a fixture does not churn in every diff.

Redaction at every depth

A macro reads what reflection cannot: @LiteralRedact, @LiteralRename, @LiteralIgnore. A redacted property three structs down is still redacted.

Formatted like your code

Output goes through swift-format with your .editorconfig or .swift-format, so the diff is about the data.

Code tour

Four things you will do.

Write the file next to the test that called it, or get the text and put it somewhere yourself. write does nothing in a release build.

Capture.swift
import SwiftLiteral

try Literal.write(
    order,
    named: "shippedOrder",
    header: "// Generated. Do not edit by hand.",
    context: "The order the pricing tests use.",
    additionalImports: ["Models"]
)

let code = try Literal.source(of: order, named: "shippedOrder")

Reflection sees stored properties and nothing else. The facts that live in the source, a secret, a different initializer label, a cache you do not want in the file, a macro reads at compile time.

User.swift
import SwiftLiteral

@SwiftLiteral(folder: "Fixtures")
struct User {
    let id: String

    @LiteralRename("displayName")
    let name: String

    @LiteralRedact(.mask("***"))
    let apiKey: String

    @LiteralIgnore
    let cache: [String: Int]
}

try user.writeLiteral(named: "testUser")
// static let testUser: User = User(id: "1", displayName: "Alice", apiKey: "***")

When the stored properties are not what the initializer takes, say what to write instead. Renderers travel in the render context, so two tests can disagree and neither has to clean up.

Phone.swift
import SwiftLiteral
import SwiftSyntax

struct Phone {
    private let digits: [Int]
    var e164: String { "+" + digits.map(String.init).joined() }
    init(e164: String) { digits = e164.compactMap(\.wholeNumberValue) }
}

LiteralConfig.registerRenderer(Phone.self) { phone, _ in
    "Phone(e164: \(literal: phone.e164))"
}

A render reads its configuration from a dependency whose test value is the library defaults. Scope anything else to the test; the override is a task local, so it reaches no other test.

FixtureTests.swift
import Dependencies
import SwiftLiteral
import Testing

@Test func twoSpaceFixture() throws {
    let code = try withDependencies {
        $0.literalConfiguration.formatProfile = { FormatProfile(indentSize: 2) }
    } operation: {
        try Literal.source(of: Order.shippedOrder, named: "fixture")
    }
    #expect(code.contains("\n  static let fixture"))
}

What it costs

Worse than JSON at four things.

If your fixtures are shared across languages, or change more often than the types do, JSON is the better trade.

It breaks the build

That is the feature, and it is also the cost. Rename a property mid-refactor and every fixture stops compiling before you are ready. JSON lets you defer.

It compiles

Fixtures are source. A large one is a large file the compiler type-checks on every build. JSON costs nothing until it is read.

Editing means regenerating

Changing one number in JSON takes seconds. In a fixture it means editing generated code by hand, or running the capture again.

It is Swift only

A JSON fixture can go to a backend team or a bug report. A Swift fixture cannot leave the project.

It also cannot write a cycle (source is a tree), read @Published or SwiftUI's @State, or invent an initializer the type does not have. It says so instead of guessing. Every limit.

Numbers

Fast enough to forget about.

1.2 msfor a whole fixture file of one order: render, swift-format and post-processing
84 msto render 1,000 orders, about 35,000 values, as one expression
2×faster Literal.source than 0.4.0 on a 1,000-order fixture, same output byte for byte
220+tests, including a swiftc -typecheck over every sample
Benchmark (median, Apple silicon)0.4.00.4.1
Literal.source, one order2.10 ms1.19 ms
Literal.source, 1,000 orders1.59 s0.76 s
Render 1,000 orders141 ms84 ms
Render a dictionary of 1,000 orders149 ms86 ms
Render 2,000 strings with escapes9.0 ms5.5 ms
Render an enum tree 40 deep944 µs405 µs

Measured with package-benchmark. How to reproduce, and what paid off: docs/performance.md.

Install

One package, one import.

Package.swift
.package(url: "https://github.com/mackoj/swift-snapshot.git", from: "0.4.1")
Target dependency
.product(name: "SwiftLiteral", package: "swift-snapshot")
  1. Get a value: run the app, decode a response, reproduce the bug.
  2. Call try Literal.write(value, named: "…") in a test, a preview or a debug button.
  3. Commit the file in __Snapshots__, and use it as Type.name.
  • Swift 6.0. macOS 13, iOS 16, tvOS 16, watchOS 9. CI builds every one.
  • Generate on macOS or in the simulator: that is where your source tree is.
  • SwiftLiteralReflection is a product on its own: value in, ExprSyntax out, no files.