mackoj/swift-snapshot
> [!WARNING]
Installation
Swift Package Manager
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
Requirements
- Swift 5.9+
- macOS (currently macOS-only)
- iOS 16+
Features
Core Capabilities
- 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
Supported Types
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
Macro Enhancements
Optional compile-time macros add:
@SwiftSnapshot- Type-level fixture support@SnapshotIgnore- Exclude properties@SnapshotRedact- Mask sensitive values@SnapshotRename- Change property names
Motivation
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 |
Learn More
- 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
Usage
Basic Export
let user = User(id: 42, name: "Alice", role: .admin)
SwiftSnapshotRuntime.export(
instance: user,
variableName: "testUser"
)
// Use fixture
let reference = User.testUserWith Documentation
SwiftSnapshotRuntime.export(
instance: product,
variableName: "sampleProduct",
header: "// Test Fixtures",
context: "Standard product fixture for pricing tests"
)Custom Output
SwiftSnapshotRuntime.export(
instance: user,
variableName: "testUser",
outputBasePath: "/path/to/fixtures",
fileName: "UserFixtures"
)With Macros
@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")Custom Renderers
SnapshotRendererRegistry.register(MyType.self) { value, context in
ExprSyntax(stringLiteral: "MyType(value: \"\(value.property)\")")
}In Tests
class Tests: XCTestCase {
func testFeature() {
let state = captureState()
state.exportSnapshot(variableName: "testState")
// Use in other tests
XCTAssertEqual(State.testState.isValid, true)
}
}In SwiftUI Previews
#Preview {
UserView(user: .testUser)
}Example: The Refactoring Problem
// 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 itConfiguration
Global Settings
SwiftSnapshotConfig.setGlobalRoot(URL(fileURLWithPath: "./Fixtures"))
SwiftSnapshotConfig.setGlobalHeader("// Test Fixtures")Formatting
// 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)Dependency Injection
For isolated test configuration:
import Dependencies
withDependencies {
$0.swiftSnapshotConfig = .init(
getGlobalRoot: { URL(fileURLWithPath: "/tmp/fixtures") },
// ... other overrides
)
} operation: {
// Tests with custom config
}DEBUG Only Architecture
SwiftSnapshot follows the same philosophy as swift-dependencies and xctest-dynamic-overlay:
Development tools should not affect production code.
How It Works
- 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/OContributing
Contributions welcome! For major changes, please open an issue first.
Acknowledgments
Built with:
- swift-syntax - Code generation
- swift-format - Formatting
- swift-dependencies - Dependency injection
- swift-issue-reporting - Error messages
Inspired by swift-snapshot-testing
License
MIT - See LICENSE for details
Package Metadata
Repository: mackoj/swift-snapshot
Default branch: main
README: README.md