Skip to content

SwiftSnapshot converts in-memory objects into compilable Swift code that you can commit, diff, and reuse anywhere: no JSON, no decoding, just Swift.

License

Notifications You must be signed in to change notification settings

mackoj/swift-snapshot

Repository files navigation

SwiftSnapshot

Swift Platform License

Warning

This is a work in progress everything is not ready yet some feature may be buggy or work in very limited use cases.

logo

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.


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 nil handling

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


Usage

Basic Export

let user = User(id: 42, name: "Alice", role: .admin)

SwiftSnapshotRuntime.export(
    instance: user,
    variableName: "testUser"
)

// Use fixture
let reference = User.testUser

With 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 it

Configuration

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/O

Contributing

Contributions welcome! For major changes, please open an issue first.

Acknowledgments

Built with:

Inspired by swift-snapshot-testing

License

MIT - See LICENSE for details

About

SwiftSnapshot converts in-memory objects into compilable Swift code that you can commit, diff, and reuse anywhere: no JSON, no decoding, just Swift.

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •