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/loulanyue-swift-protocol-di-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 23% | 0% |
| case-19 | ✗→✓ | ▲ Improved | 36% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 31% | 0% |
| case-11 | ✓→✓ | = Same ✓ | 39% | 0% |
Patterns for making Swift code testable by abstracting external dependencies (file system, network, iCloud) behind small, focused protocols. Enables deterministic tests without I/O.
Each protocol handles exactly one external concern.
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 } }
Production code uses defaults; tests inject mocks.
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 conditionals instead of proper dependency injectionSendable conformance when used with actors| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | pass→pass | 14,860 | 11,960 | -20% | 1 | 1 | 0% | 2,543 | 3,529 | +39% | 0 | 0 | — |
case-10 | pass→pass | 10,696 | 7,285 | -32% | 1 | 1 | 0% | 2,110 | 2,914 | +38% | 0 | 0 | — |
case-01 | fail→pass | 16,227 | 8,963 | -45% | 1 | 1 | 0% | 3,377 | 3,311 | -2% | 0 | 0 | — |
case-02 | pass→pass | 18,411 | 15,759 | -14% | 1 | 1 | 0% | 3,614 | 4,939 | +37% | 0 | 0 | — |
case-03 | fail→pass | 16,146 | 12,188 | -25% | 1 | 1 | 0% | 3,282 | 4,039 | +23% | 0 | 0 | — |
case-04 | pass→pass | 12,964 | 8,784 | -32% | 1 | 1 | 0% | 2,403 | 3,092 | +29% | 0 | 0 | — |
case-05 | pass→pass | 19,663 | 16,273 | -17% | 1 | 1 | 0% | 3,614 | 4,783 | +32% | 0 | 0 | — |
case-06 | pass→pass | 9,629 | 5,441 | -43% | 1 | 1 | 0% | 1,886 | 2,511 | +33% | 0 | 0 | — |
case-07 | pass→pass | 8,686 | 6,574 | -24% | 1 | 1 | 0% | 1,835 | 2,752 | +50% | 0 | 0 | — |
case-08 | pass→pass | 11,792 | 11,370 | -4% | 1 | 1 | 0% | 2,217 | 3,551 | +60% | 0 | 0 | — |
case-09 | pass→pass | 11,518 | 4,760 | -59% | 1 | 1 | 0% | 1,892 | 2,305 | +22% | 0 | 0 | — |
case-12 | pass→pass | 16,457 | 13,582 | -17% | 1 | 1 | 0% | 3,366 | 4,348 | +29% | 0 | 0 | — |
case-13 | pass→pass | 8,675 | 5,578 | -36% | 1 | 1 | 0% | 1,773 | 2,440 | +38% | 0 | 0 | — |
case-14 | pass→pass | 13,770 | 13,745 | -0% | 1 | 1 | 0% | 2,351 | 3,714 | +58% | 0 | 0 | — |
case-15 | pass→pass | 8,208 | 5,381 | -34% | 1 | 1 | 0% | 1,622 | 2,325 | +43% | 0 | 0 | — |
case-16 | pass→pass | 13,915 | 7,101 | -49% | 1 | 1 | 0% | 2,478 | 2,643 | +7% | 0 | 0 | — |
case-17 | pass→pass | 13,084 | 8,271 | -37% | 1 | 1 | 0% | 2,272 | 2,855 | +26% | 0 | 0 | — |
case-18 | pass→pass | 14,514 | 11,578 | -20% | 1 | 1 | 0% | 2,680 | 3,570 | +33% | 0 | 0 | — |
case-19 | fail→pass | 12,196 | 7,836 | -36% | 1 | 1 | 0% | 2,120 | 2,875 | +36% | 0 | 0 | — |
case-20 | pass→pass | 12,283 | 8,735 | -29% | 1 | 1 | 0% | 2,346 | 3,197 | +36% | 0 | 0 | — |
case-21 | pass→pass | 12,523 | 11,984 | -4% | 1 | 1 | 0% | 2,327 | 3,687 | +58% | 0 | 0 | — |
case-22 | fail→pass | 10,672 | 6,428 | -40% | 1 | 1 | 0% | 1,909 | 2,503 | +31% | 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 +18 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.