Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.
.claude/skills/affaan-m-swift-protocol-di-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 32% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 13% | 0% |
| case-02 | ✗→✓ | ▲ Improved | -25% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 40% | 0% |
通过将外部依赖(文件系统、网络、iCloud)抽象为小型、专注的协议,使 Swift 代码可测试的模式。支持无需 I/O 的确定性测试。
每个协议仅处理一个外部关注点。
swift// 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? }
swiftpublic 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) } }
swiftpublic 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 } }
生产代码使用默认值;测试注入模拟对象。
swiftpublic 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... } }
swiftimport 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() } }
#if DEBUG 条件语句代替适当的依赖注入Sendable 一致性| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 11,272 | 7,561 | -33% | 1 | 1 | 0% | 2,109 | 2,806 | +33% | 0 | 0 | — |
case-03 | pass→pass | 14,523 | 13,977 | -4% | 1 | 1 | 0% | 2,766 | 4,494 | +62% | 0 | 0 | — |
case-06 | fail→pass | 13,186 | 17,793 | +35% | 1 | 1 | 0% | 2,313 | 3,641 | +57% | 0 | 0 | — |
case-07 | fail→pass | 14,670 | 11,388 | -22% | 1 | 1 | 0% | 2,989 | 3,954 | +32% | 0 | 0 | — |
case-04 | pass→pass | 10,670 | 11,027 | +3% | 1 | 1 | 0% | 1,973 | 3,434 | +74% | 0 | 0 | — |
case-01 | fail→pass | 14,972 | 10,853 | -28% | 1 | 1 | 0% | 3,481 | 3,925 | +13% | 0 | 0 | — |
case-02 | fail→pass | 23,783 | 12,991 | -45% | 1 | 1 | 0% | 5,603 | 4,178 | -25% | 0 | 0 | — |
case-08 | fail→pass | 9,311 | 4,386 | -53% | 1 | 1 | 0% | 1,543 | 2,157 | +40% | 0 | 0 | — |
case-09 | fail→pass | 12,584 | 10,617 | -16% | 1 | 1 | 0% | 2,186 | 3,429 | +57% | 0 | 0 | — |
case-10 | pass→pass | 15,247 | 10,799 | -29% | 1 | 1 | 0% | 2,980 | 3,810 | +28% | 0 | 0 | — |
case-11 | pass→pass | 12,643 | 9,976 | -21% | 1 | 1 | 0% | 2,275 | 3,450 | +52% | 0 | 0 | — |
case-12 | pass→pass | 10,967 | 7,686 | -30% | 1 | 1 | 0% | 2,314 | 2,993 | +29% | 0 | 0 | — |
case-13 | fail→fail | 11,937 | 14,664 | +23% | 1 | 1 | 0% | 2,451 | 4,184 | +71% | 0 | 0 | — |
case-14 | pass→pass | 13,991 | 8,181 | -42% | 1 | 1 | 0% | 2,777 | 3,031 | +9% | 0 | 0 | — |
case-15 | fail→pass | 13,134 | 10,334 | -21% | 1 | 1 | 0% | 2,600 | 3,553 | +37% | 0 | 0 | — |
case-16 | fail→pass | 14,348 | 15,768 | +10% | 1 | 1 | 0% | 3,101 | 4,888 | +58% | 0 | 0 | — |
case-17 | fail→pass | 14,587 | 8,001 | -45% | 1 | 1 | 0% | 2,754 | 3,155 | +15% | 0 | 0 | — |
case-18 | pass→pass | 14,872 | 7,526 | -49% | 1 | 1 | 0% | 3,032 | 2,931 | -3% | 0 | 0 | — |
case-19 | fail→pass | 12,196 | 6,647 | -45% | 1 | 1 | 0% | 2,306 | 2,643 | +15% | 0 | 0 | — |
case-20 | pass→pass | 9,951 | 6,293 | -37% | 1 | 1 | 0% | 1,771 | 2,562 | +45% | 0 | 0 | — |
case-21 | pass→pass | 11,694 | 7,793 | -33% | 1 | 1 | 0% | 2,093 | 2,795 | +34% | 0 | 0 | — |
case-22 | pass→pass | 11,678 | 7,517 | -36% | 1 | 1 | 0% | 1,953 | 2,713 | +39% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +45 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.