Swift Protocol Di Testing

affaan-m/ECC/skills/swift-protocol-di-testing

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775無授權條款275K 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫4 天前更新

Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing. Use when Swift code needs testing and file system, network, or external APIs must be mocked.

AI 產生的概覽

指導 Swift 開發者使用基於協定的依賴注入,模擬檔案系統、網路和外部 API 以便測試。

功能
此技能提供將 Swift 中的外部依賴抽象為小型、專注協定的模式。它展示如何定義協定、建立預設生產實作、建構可設定錯誤的模擬實作,並透過預設參數注入依賴。它也示範如何使用 Swift Testing 撰寫確定性測試,包括錯誤路徑模擬。
適用情境
適用於撰寫存取檔案系統、網路或外部 API 且需要可測試的 Swift 程式碼時。它適合測試在真實環境中難以觸發的錯誤處理路徑,以及建構可在應用程式、測試和 SwiftUI 預覽環境中運作的模組。
執行需求
需要 Swift 和 Swift Testing 框架。不包含指令碼,僅為說明文件。

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.

swift
// File system accesspublic protocol FileSystemProviding: Sendable {    func containerURL(for purpose: Purpose) -> URL?}
// File read/write operationspublic 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

swift
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

swift
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.

swift
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

swift
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

來源與署名

來源:affaan-m/ECC位於skills/swift-protocol-di-testing提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架