Swift Protocol-Based Dependency Injection for Testing

SkillFiles & storage

This skill helps an agent write testable Swift code by mocking the file system, network, and external APIs through protocols. It applies protocol-based dependency injection so that production types are replaced with focused test doubles in Swift Testing. Use it when Swift code needs testing and those dependencies must be mocked.

Use Swift Protocol-Based Dependency Injection for Testing in Claude, ChatGPT or Ahel Desktop

Free. Sign in, add Swift Protocol-Based Dependency Injection for Testing and connect your AI. About a minute.

Also: Claude Code · Cursor · Codex

Then ask your AI: use the Swift Protocol-Based Dependency Injection for Testing skill

Details

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Have a Swift project with code that touches the file system, network, or external APIs.

Swift Protocol-Based Dependency Injection for TestingStart free

What your AI can do with it

  • Define focused protocols for file system, network, and API access
  • Inject protocol dependencies into Swift types instead of concrete ones
  • Create mock implementations that conform to those protocols
  • Write Swift Testing tests that use the mocks
  • Replace real file system, network, and external API calls in tests

Getting started

  1. Have a Swift project with code that touches the file system, network, or external APIs.
  2. Identify each dependency and declare a focused protocol for the operations the code needs.
  3. Change the production types to accept those protocols instead of concrete implementations.
  4. Add mock types that conform to the protocols for use in tests.
  5. Write Swift Testing tests that pass the mocks into the code under test.

What this skill tells your AI

The instructions your AI receives, as published by affaan-m/ecc in skills/swift-protocol-di-testing/SKILL.md and read by ahel’s review.

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

Signals

GitHub stars
270k
Forks
40k
Last commit
Sep 2026

Questions

Does this skill support mocking file system, network, and external APIs?
Yes. It covers mocking the file system, network, and external APIs by defining focused protocols and injecting mock implementations into Swift types.
Which testing framework does it use?
It uses Swift Testing for the tests that exercise the injected mock dependencies.
Can it mock dependencies in existing Swift code?
Yes. It works by introducing protocols for the dependencies the code already uses and replacing concrete types with mocks in tests.
Does it require a specific dependency injection container?
No. It relies on plain protocol-based injection, so no container is needed.
Advanced
Item type
skill
Key
swift-protocol-di-testing
Source
github.com/affaan-m/ecc