Appearance
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
}
}技巧:
- 通过设置
readError、writeError,可以在测试中轻松模拟各种异常
步骤 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。