PluginBench
Skill
Pass
Audit score 90

swift-protocol-di-testing

affaan-m/ecc

Protocol-based dependency injection for testable Swift code with mocks and Swift Testing.

What is swift-protocol-di-testing?

Patterns for abstracting external dependencies (file system, network, APIs) behind focused protocols in Swift. Enables deterministic testing without real I/O and works across app, test, and preview contexts using Swift concurrency.

  • Define small, single-responsibility protocols for each external concern (file system, network, bookmarks)
  • Create production implementations using real FileManager and network calls
  • Build mock implementations with configurable errors for testing failure paths
  • Inject dependencies via default parameters so tests override with mocks
  • Write deterministic tests using Swift Testing without triggering real I/O

How to install swift-protocol-di-testing

npx skills add null --skill swift-protocol-di-testing
Prerequisites
  • Swift 5.9+ (for Swift Testing framework)
  • Understanding of Swift protocols and dependency injection basics
  • Familiarity with Swift concurrency (actors, Sendable) recommended
Claude Code
Cursor
Windsurf
Cline

How to use swift-protocol-di-testing

  1. 1.Define focused protocols for each external dependency (one concern per protocol)
  2. 2.Implement production versions using real FileManager, URLSession, or APIs
  3. 3.Create mock versions with configurable error properties and in-memory storage
  4. 4.Add protocols as init parameters with default production implementations
  5. 5.Write tests injecting mocks and verifying behavior with Swift Testing assertions

Use cases

Good for
  • Testing error handling for file system failures without corrupting real files
  • Mocking network requests in unit tests to avoid external API calls
  • Testing iCloud/bookmark storage logic in isolation
  • Validating actor-based code paths with Sendable-conforming mocks
  • Testing SwiftUI previews with fake data without side effects
Who it's for
  • Swift app developers building testable architectures
  • Teams using Swift concurrency (actors, structured concurrency)
  • Engineers testing error paths that are hard to trigger in production
  • Module authors supporting app, test, and preview contexts

swift-protocol-di-testing FAQ

Should I mock internal types that have no external dependencies?

No. Only mock external boundaries (file system, network, APIs). Internal types without external dependencies don't need protocols or mocks.

Why use small focused protocols instead of one large protocol?

Single-responsibility protocols are easier to mock, test, and reuse. Large protocols become hard to implement and test completely.

Do mocks need to be Sendable?

Yes, if the protocol is used across actor boundaries. Mark mocks with `@unchecked Sendable` only when you've verified thread safety manually.

Can I use #if DEBUG instead of dependency injection?

No. Conditional compilation hides test code from production and makes architecture unclear. Use proper DI with default parameters instead.

How do I test error paths without real failures?

Add error properties to mocks (e.g., `readError: Error?`) and set them in tests before calling the code under test.

Full instructions (SKILL.md)

Source of truth, from affaan-m/ecc.


name: swift-protocol-di-testing description: Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing. metadata: origin: ECC

Swift Protocol-Based Dependency Injection for Testing

Patterns for making Swift code testable by abstracting external dependencies (file system, network, iCloud) behind small, focused protocols. Enables deterministic tests without I/O.

When to Activate

  • Writing Swift code that accesses file system, network, or external APIs
  • Need to test error handling paths without triggering real failures
  • Building modules that work across environments (app, test, SwiftUI preview)
  • Designing testable architecture with Swift concurrency (actors, Sendable)

Core Pattern

1. Define Small, Focused Protocols

Each protocol handles exactly one external concern.

// File system access
public protocol FileSystemProviding: Sendable {
    func containerURL(for purpose: Purpose) -> URL?
}

// File read/write operations
public protocol FileAccessorProviding: Sendable {
    func read(from url: URL) throws -> Data
    func write(_ data: Data, to url: URL) throws
    func fileExists(at url: URL) -> Bool
}

// Bookmark storage (e.g., for sandboxed apps)
public protocol BookmarkStorageProviding: Sendable {
    func saveBookmark(_ data: Data, for key: String) throws
    func loadBookmark(for key: String) throws -> Data?
}

2. Create Default (Production) Implementations

public struct DefaultFileSystemProvider: FileSystemProviding {
    public init() {}

    public func containerURL(for purpose: Purpose) -> URL? {
        FileManager.default.url(forUbiquityContainerIdentifier: nil)
    }
}

public struct DefaultFileAccessor: FileAccessorProviding {
    public init() {}

    public func read(from url: URL) throws -> Data {
        try Data(contentsOf: url)
    }

    public func write(_ data: Data, to url: URL) throws {
        try data.write(to: url, options: .atomic)
    }

    public func fileExists(at url: URL) -> Bool {
        FileManager.default.fileExists(atPath: url.path)
    }
}

3. Create Mock Implementations for Testing

public final class MockFileAccessor: FileAccessorProviding, @unchecked Sendable {
    public var files: [URL: Data] = [:]
    public var readError: Error?
    public var writeError: Error?

    public init() {}

    public func read(from url: URL) throws -> Data {
        if let error = readError { throw error }
        guard let data = files[url] else {
            throw CocoaError(.fileReadNoSuchFile)
        }
        return data
    }

    public func write(_ data: Data, to url: URL) throws {
        if let error = writeError { throw error }
        files[url] = data
    }

    public func fileExists(at url: URL) -> Bool {
        files[url] != nil
    }
}

4. Inject Dependencies with Default Parameters

Production code uses defaults; tests inject mocks.

public actor SyncManager {
    private let fileSystem: FileSystemProviding
    private let fileAccessor: FileAccessorProviding

    public init(
        fileSystem: FileSystemProviding = DefaultFileSystemProvider(),
        fileAccessor: FileAccessorProviding = DefaultFileAccessor()
    ) {
        self.fileSystem = fileSystem
        self.fileAccessor = fileAccessor
    }

    public func sync() async throws {
        guard let containerURL = fileSystem.containerURL(for: .sync) else {
            throw SyncError.containerNotAvailable
        }
        let data = try fileAccessor.read(
            from: containerURL.appendingPathComponent("data.json")
        )
        // Process data...
    }
}

5. Write Tests with Swift Testing

import Testing

@Test("Sync manager handles missing container")
func testMissingContainer() async {
    let mockFileSystem = MockFileSystemProvider(containerURL: nil)
    let manager = SyncManager(fileSystem: mockFileSystem)

    await #expect(throws: SyncError.containerNotAvailable) {
        try await manager.sync()
    }
}

@Test("Sync manager reads data correctly")
func testReadData() async throws {
    let mockFileAccessor = MockFileAccessor()
    mockFileAccessor.files[testURL] = testData

    let manager = SyncManager(fileAccessor: mockFileAccessor)
    let result = try await manager.loadData()

    #expect(result == expectedData)
}

@Test("Sync manager handles read errors gracefully")
func testReadError() async {
    let mockFileAccessor = MockFileAccessor()
    mockFileAccessor.readError = CocoaError(.fileReadCorruptFile)

    let manager = SyncManager(fileAccessor: mockFileAccessor)

    await #expect(throws: SyncError.self) {
        try await manager.sync()
    }
}

Best Practices

  • Single Responsibility: Each protocol should handle one concern — don't create "god protocols" with many methods
  • Sendable conformance: Required when protocols are used across actor boundaries
  • Default parameters: Let production code use real implementations by default; only tests need to specify mocks
  • Error simulation: Design mocks with configurable error properties for testing failure paths
  • Only mock boundaries: Mock external dependencies (file system, network, APIs), not internal types

Anti-Patterns to Avoid

  • Creating a single large protocol that covers all external access
  • Mocking internal types that have no external dependencies
  • Using #if DEBUG conditionals instead of proper dependency injection
  • Forgetting Sendable conformance when used with actors
  • Over-engineering: if a type has no external dependencies, it doesn't need a protocol

When to Use

  • Any Swift code that touches file system, network, or external APIs
  • Testing error handling paths that are hard to trigger in real environments
  • Building modules that need to work in app, test, and SwiftUI preview contexts
  • Apps using Swift concurrency (actors, structured concurrency) that need testable architecture