Warning
This is a work in progress everything is not ready yet some feature may be buggy or work in very limited use cases.
Generate type-safe Swift source fixtures from runtime values.
SwiftSnapshot converts in-memory objects into compilable Swift code that you can commit, diff, and reuse anywhere: no JSON, no decoding, just Swift.
let user = User(id: 42, name: "Alice", role: .admin)
user.exportSnapshot(variableName: "testUser")
// Creates: User+testUser.swift
// extension User {
// static let testUser: User = User(
// id: 42,
// name: "Alice",
// role: .admin
// )
// }This project was built with help of Copilot. I wanted to tests it's capabilities. This shows that, with proper guidance, it can create something that works.
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/mackoj/swift-snapshot.git", from: "0.1.0")
]Or in Xcode: File → Add Packages → Enter repository URL
- Swift 5.9+
- macOS (currently macOS-only)
- iOS 16+
- Type-Safe Generation - Compiler-verified fixtures
- Broad Type Support - Primitives, collections, Foundation types, custom types
- Custom Renderers - Extensible type handling
- Deterministic Output - Sorted keys, stable ordering
- Smart Formatting - EditorConfig and swift-format integration
- Thread-Safe - Concurrent exports supported
- DEBUG-Only - Zero production overhead
Built-in:
- Primitives:
String,Int,Double,Float,Bool,Character - Collections:
Array,Dictionary,Set - Foundation:
Date,UUID,URL,Data,Decimal - Optionals: Automatic
nilhandling
Custom Types:
- Structs and classes via reflection
- Enums with associated values
- Nested structures
- User-defined via custom renderers
Optional compile-time macros add:
@SwiftSnapshot- Type-level fixture support@SnapshotIgnore- Exclude properties@SnapshotRedact- Mask sensitive values@SnapshotRename- Change property names
Traditional test fixtures have problems:
| Problem | SwiftSnapshot Solution |
|---|---|
| JSON fixtures break silently when types change | Compiler-verified - won't build if types change |
| Hardcoded test data scattered across files | Centralized fixtures with single source of truth |
| Binary snapshots have opaque diffs | Human-readable diffs in version control |
| Decoding overhead in every test | Zero overhead - use fixtures directly |
| No IDE support for fixture data | Full autocomplete and navigation |
- What is SwiftSnapshot and Why? - Purpose and motivation
- Architecture - Technical design
- Basic Usage - Examples and patterns
- Custom Renderers - Type-specific rendering
- Formatting Configuration - Code style setup
let user = User(id: 42, name: "Alice", role: .admin)
SwiftSnapshotRuntime.export(
instance: user,
variableName: "testUser"
)
// Use fixture
let reference = User.testUserSwiftSnapshotRuntime.export(
instance: product,
variableName: "sampleProduct",
header: "// Test Fixtures",
context: "Standard product fixture for pricing tests"
)SwiftSnapshotRuntime.export(
instance: user,
variableName: "testUser",
outputBasePath: "/path/to/fixtures",
fileName: "UserFixtures"
)@SwiftSnapshot(folder: "Fixtures")
struct User {
let id: String
@SnapshotRename("displayName")
let name: String
@SnapshotRedact(.mask("***"))
let apiKey: String
@SnapshotIgnore
let cache: [String: Any]
}
user.exportSnapshot(variableName: "testUser")SnapshotRendererRegistry.register(MyType.self) { value, context in
ExprSyntax(stringLiteral: "MyType(value: \"\(value.property)\")")
}class Tests: XCTestCase {
func testFeature() {
let state = captureState()
state.exportSnapshot(variableName: "testState")
// Use in other tests
XCTAssertEqual(State.testState.isValid, true)
}
}#Preview {
UserView(user: .testUser)
}// You rename a property
struct User {
- let name: String
+ let fullName: String
}
// ❌ JSON fixtures: Silent runtime failure
{"name": "Alice"} // Still has old property name
// ✅ Swift fixtures: Compile-time error
User(name: "Alice") // Error: No parameter 'name'
// Compiler guides you to fix itSwiftSnapshotConfig.setGlobalRoot(URL(fileURLWithPath: "./Fixtures"))
SwiftSnapshotConfig.setGlobalHeader("// Test Fixtures")// From .editorconfig
SwiftSnapshotConfig.setFormatConfigSource(
.editorconfig(URL(fileURLWithPath: ".editorconfig"))
)
// Or manual
let profile = FormatProfile(
indentStyle: .space,
indentSize: 2,
endOfLine: .lf,
insertFinalNewline: true,
trimTrailingWhitespace: true
)
SwiftSnapshotConfig.setFormattingProfile(profile)For isolated test configuration:
import Dependencies
withDependencies {
$0.swiftSnapshotConfig = .init(
getGlobalRoot: { URL(fileURLWithPath: "/tmp/fixtures") },
// ... other overrides
)
} operation: {
// Tests with custom config
}SwiftSnapshot follows the same philosophy as swift-dependencies and xctest-dynamic-overlay:
Development tools should not affect production code.
- DEBUG builds: Full functionality
- RELEASE builds: APIs become no-ops
- Result: Zero production overhead
// Safe to leave in codebase
let url = user.exportSnapshot()
// DEBUG: Creates file
// RELEASE: Returns placeholder, no I/OContributions welcome! For major changes, please open an issue first.
Built with:
- swift-syntax - Code generation
- swift-format - Formatting
- swift-dependencies - Dependency injection
- swift-issue-reporting - Error messages
Inspired by swift-snapshot-testing
MIT - See LICENSE for details