Skip to content

Swift Protocol DI Testing Skill 让 Swift 代码通过协议化依赖注入(Protocol-Based Dependency Injection),实现对文件系统、网络、iCloud 等外部依赖的解耦和 Mock,结合 Swift Testing 框架,实现无副作用、可控的自动化测试。它适用于需要测试异常分支、并发 Actor、跨环境模块的场景,是 Claude Code 体系下 Swift 项目实现高可测性架构的关键技能。本文将详解 Skill 的激活条件、分步用法、输出示例及最佳实践,帮助你在实际项目中高效落地。

Everything Claude Code Swift Protocol DI Testing Skill:协议依赖注入与 Swift Testing Mock 可测试架构

在 Swift 项目中,如何让代码既能访问真实的文件系统、网络、iCloud 等外部资源,又能在测试环境下彻底隔离副作用,实现高效、可控的 Mock 测试?传统做法往往直接调用 FileManager、URLSession 等系统 API,导致测试难以覆盖异常分支、环境不可控、代码难以复用。Swift Protocol DI Testing Skill 正是为了解决这一痛点,通过协议化依赖注入(Protocol-Based Dependency Injection),让你的 Swift 代码天然具备可 Mock、可测试、可扩展的架构能力。

本 Skill 已在 Claude Code 等 AI 编程助手的生产级插件体系中广泛应用,适用于希望系统性提升 Swift 测试质量、架构可维护性的开发者。你可以结合 Everything Claude Code 完全指南 了解更多整体架构与技能协作模式。

1. 典型痛点与 Skill 价值

不用本 Skill 时的常见问题:

  • 直接依赖 FileManager、URLSession 等系统 API,测试时无法替换或 Mock,导致测试覆盖率低、异常分支难以验证
  • 代码与外部依赖高度耦合,难以在 app、测试、SwiftUI Preview 等多环境下复用
  • 并发(Actor)场景下,依赖未实现 Sendable,容易出现线程安全和数据竞争问题
  • 只能用 #if DEBUG 条件编译区分测试与生产,架构混乱、易出错

Swift Protocol DI Testing Skill 解决方案:

  • 通过“小而专一”的协议抽象所有外部依赖,每个协议只负责一个外部关注点
  • 生产环境用默认实现,测试环境注入 Mock 实现,彻底隔离副作用
  • Mock 支持可配置的错误模拟,轻松测试各种异常分支
  • 协议强制 Sendable,天然支持 Actor/并发安全
  • 支持自动化测试、SwiftUI Preview、Module 解耦等多场景

2. 触发条件(什么时候用)

  • 你的 Swift 代码需要访问文件系统、网络、iCloud 或外部 API
  • 希望测试异常分支(如文件不存在、读写失败、网络超时等),但不希望触发真实 I/O
  • 需要让同一模块在 app、测试、SwiftUI Preview 等多环境下无缝切换
  • 构建基于 Actor 的并发架构,需要依赖 Sendable 的协议
  • 希望提升代码的可维护性、可测试性和可扩展性

3. 分步操作指南(Step by Step)

步骤 1:为每个外部依赖定义“小而专一”的协议

每个协议只负责一个外部关注点。例如:

swift
// 文件系统定位
public protocol FileSystemProviding: Sendable {
    func containerURL(for purpose: Purpose) -> URL?
}

// 文件读写
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
}

// 书签存储(如沙盒 App)
public protocol BookmarkStorageProviding: Sendable {
    func saveBookmark(_ data: Data, for key: String) throws
    func loadBookmark(for key: String) throws -> Data?
}

最佳实践:

  • 每个协议只负责一件事,避免出现“上帝协议”
  • 明确 Sendable,方便 Actor/并发场景安全使用

步骤 2:实现默认(生产环境)实现

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

public struct DefaultFileAccessor: FileAccessorProviding {
    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:实现 Mock 版本,用于测试

Mock 实现支持可配置的错误模拟和虚拟文件系统:

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

    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
    }
}

技巧:

  • 通过设置 readErrorwriteError,可以在测试中轻松模拟各种异常

步骤 4:在业务类型中通过依赖注入(带默认参数)使用协议

生产环境用默认实现,测试时注入 Mock:

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")
        )
        // ...后续业务逻辑
    }
}

步骤 5:用 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()
    }
}

输出示例:

  • 测试能覆盖文件不存在、读写失败、数据正确性等场景
  • 无需真实文件操作,测试速度极快、结果可复现

4. 常见配套 Agent 与 Skill 协作

  • 可与 TDD Guide Agent 配合,实现测试先行、覆盖率强制的开发流程
  • 结合 Swift Actor Persistence Skill ,可进一步提升并发安全和数据一致性
  • 在多 Agent 协作、自动化验证等场景下,作为 Swift 测试架构的基础能力被广泛复用
  • Verification Loop 等自动化验证体系协同,实现端到端的测试闭环

5. 最佳实践与注意事项

  • 单一职责:每个协议只负责一个外部关注点,避免“巨型协议”
  • Sendable 合规:协议与实现需标注 Sendable,确保 Actor/并发安全
  • 默认参数注入:生产代码用默认实现,测试时才注入 Mock,保证代码简洁
  • 只 Mock 边界:只 Mock 文件系统、网络等外部依赖,不要 Mock 内部纯逻辑类型
  • 错误模拟能力:Mock 支持自定义错误,方便覆盖异常分支
  • 避免反模式:不要用 #if DEBUG 区分测试/生产,不要为无外部依赖的类型强行加协议

更多高级技巧可参考 Claude Code 高级技巧:Token 优化、记忆持久化、并行化与验证循环


FAQ

Q: 这种协议依赖注入模式适合所有 Swift 项目吗?
A: 只要你的代码涉及文件系统、网络、外部 API 等外部依赖,推荐采用本模式。对于纯算法、无外部依赖的类型无需强制使用。

Q: Mock 实现需要和生产实现保持一致吗?
A: Mock 只需覆盖测试关心的接口和行为,重点在于可控性和错误模拟,不必完全复刻生产实现细节。

Q: 如何保证并发安全?
A: 协议和实现需标注 Sendable,Actor 内部依赖注入时也要用 Sendable 协议,Mock 实现如无并发共享可用 @unchecked Sendable。