- 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.
let order = try JSONDecoder().decode(Order.self, from: response)
try Literal.write(order, named: "shippedOrder")
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.
{ "name": "Alice" }
Still loads. Fails later, in some test, as a decoding error.
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.
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.
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.
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.
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.
Literal.source than 0.4.0 on a 1,000-order fixture, same output byte for byteswiftc -typecheck over every sample| Benchmark (median, Apple silicon) | 0.4.0 | 0.4.1 |
|---|---|---|
Literal.source, one order | 2.10 ms | 1.19 ms |
Literal.source, 1,000 orders | 1.59 s | 0.76 s |
| Render 1,000 orders | 141 ms | 84 ms |
| Render a dictionary of 1,000 orders | 149 ms | 86 ms |
| Render 2,000 strings with escapes | 9.0 ms | 5.5 ms |
| Render an enum tree 40 deep | 944 µs | 405 µs |
Measured with package-benchmark. How to reproduce, and what paid off: docs/performance.md.
Install
One package, one import.
.package(url: "https://github.com/mackoj/swift-snapshot.git", from: "0.4.1")
.product(name: "SwiftLiteral", package: "swift-snapshot")
- Get a value: run the app, decode a response, reproduce the bug.
- Call
try Literal.write(value, named: "…")in a test, a preview or a debug button. - Commit the file in
__Snapshots__, and use it asType.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.
SwiftLiteralReflectionis a product on its own: value in,ExprSyntaxout, no files.