claude-code/claude-zh/skills/swift-protocol-di-testing/SKILL.md

80 lines
3.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: swift-protocol-di-testing
description: 使用基於協定 (Protocol) 的依賴注入 (DI) 來編寫可測試的 Swift 程式碼 — 透過聚焦的協定與 Swift Testing 模擬檔案系統、網路與外部 API。
---
# Swift 基於協定的依賴注入 (DI) 與測試實踐
這是透過將外部依賴檔案系統、網路、iCloud抽象化為小型、聚焦的協定使 Swift 程式碼具備高測試性的模式。這能實現無 I/O 的確定性測試。
## 何時啟用
- 撰寫需存取檔案系統、網路或外部 API 的 Swift 程式碼時。
- 需要測試在不觸發真實失敗的情況下,驗證錯誤處理路徑。
- 建構需跨環境App、測試、SwiftUI Preview運行的模組。
- 使用 Swift ConcurrencyActors, Sendable設計可測試架構。
## 核心模式
### 1. 定義小型且聚焦的協定
每個協定僅處理一個外部關注點。
```swift
// 檔案存取行為抽象化
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
}
```
### 2. 實作生產環境版本與測試模擬版本
- **生產版本 (Default)**:封裝真實的 `FileManager``URLSession` 操作。
- **測試版本 (Mock)**:內部使用字典或記憶體狀態,模擬檔案操作與注入預期的 `Error`
### 3. 利用預設參數進行依賴注入
在建構子中使用預設參數,讓生產環境保持簡潔,同時允許測試案例傳入 Mock
```swift
public actor SyncManager {
private let fileAccessor: FileAccessorProviding
public init(fileAccessor: FileAccessorProviding = DefaultFileAccessor()) {
self.fileAccessor = fileAccessor
}
public func load() async throws -> Data {
try fileAccessor.read(from: someURL)
}
}
```
### 4. 使用 Swift Testing 進行驗證
利用 `Swift Testing``@Test` 巨集與 `#expect` 語法驗證邏輯與例外:
```swift
@Test("驗證資料讀取錯誤處理")
func testReadError() async {
let mock = MockFileAccessor()
mock.readError = CocoaError(.fileReadCorruptFile)
let manager = SyncManager(fileAccessor: mock)
await #expect(throws: Error.self) {
try await manager.load()
}
}
```
## 實踐之最佳實踐
- **單一職責原則**:協定應儘可能微小,避免開發「上帝協定」。
- **Sendable 順應性**:由於 Actor 運算,跨邊界傳遞的協定必須標註為 `Sendable`
- **僅針對邊界進行 Mock**:僅模擬外部資源(檔案、網路),內部的邏輯類型不需過度抽象化。
- **模擬錯誤路徑**:這是 DI 最大的價值,確保系統在真實故障發生前已經過充分測試。
## 應避免的反模式
- 使用 `#if DEBUG` 條件編譯來切換邏輯,這會破壞程式碼的純粹性。
- 過度設計 (Over-engineering):若該類型沒有外部依賴或不具備副作用,不需強行加上協定層。
- 忘記 Actor 呼叫是非同步的,漏掉 `await` 關鍵字。