測試與程式碼品質

注入儲存介面,驗證單例依賴的成功與失敗

使用儲存協定與記錄用測試替身,在不連接資料庫的情況下驗證訂單內容、寫入前的輸入拒絕,以及儲存錯誤傳遞。

閱讀 2 分鐘
注入儲存介面,驗證單例依賴的成功與失敗 封面圖

障礙不只是只有一個實例,而是呼叫端直接選擇具體儲存實作。把儲存角色表達成協定並注入實作,就能在沒有網路的情況下檢查服務的判斷。

原理與設計判斷

從外部接收物件,不代表必然能替換實作。參數若仍是具體Database型別,替身也必須滿足同一型別限制。此處用OrderStore建立小型契約,服務接收any OrderStore。符合契約的單例也可以被注入。 第一手資料

重現程式碼

完整範例定義了訂單、錯誤、協定、服務及記錄型儲存替身。測試正常傳入的訂單值、空ID是否在寫入前被拒絕,以及儲存失敗是否傳到呼叫端。替身兼具記錄呼叫的spy與設定失敗行為的stub角色。

這個獨立範例已於 2026-10-23 使用 Apple Swift 6.4,以 Swift 6 語言模式在 arm64 macOS 目標環境執行。不需要網路或額外套件。

下載完整 Swift 檔案

請在下載檔案的資料夾執行。檢查不符時,precondition 會中止程式。請勿使用 -Ounchecked,因為它會移除這些檢查。

swift -swift-version 6 singleton-testing-dependency-injection.swift
struct Order: Equatable { let id: String }
enum SaveError: Error { case invalidID, unavailable }

protocol OrderStore {
    func insert(_ order: Order) throws
}

struct OrderService {
    let store: any OrderStore
    func save(_ order: Order) throws {
        guard !order.id.isEmpty else { throw SaveError.invalidID }
        try store.insert(order)
    }
}

final class RecordingStore: OrderStore {
    var calls: [Order] = []
    var shouldFail = false
    func insert(_ order: Order) throws {
        calls.append(order)
        if shouldFail { throw SaveError.unavailable }
    }
}

let store = RecordingStore()
let service = OrderService(store: store)
let order = Order(id: "order-42")
try service.save(order)
precondition(store.calls == [order])
do {
    try service.save(Order(id: ""))
    preconditionFailure("invalid input was accepted")
} catch SaveError.invalidID {}
precondition(store.calls.count == 1)
store.shouldFail = true
do {
    try service.save(order)
    preconditionFailure("storage failure was swallowed")
} catch SaveError.unavailable {}
precondition(store.calls.count == 2)
print("saved=order-42 invalid-input-writes=0 storage-error=propagated")

執行結果與解讀

saved=order-42 invalid-input-writes=0 storage-error=propagated

正常請求傳入order-42。空ID沒有增加儲存呼叫,儲存錯誤也沒有被吞掉。這裡除了呼叫次數,也檢查了傳入值與兩種失敗路徑。

適用範圍與限制

協定無法證明真實資料庫的交易、唯一性限制或網路行為正確,仍須另外做整合測試。不是所有依賴都需要抽象化,注入全域共用狀態也不會自動讓測試彼此隔離。

接下來要確認的事

從一個外部I/O邊界開始建立小型契約。確認正式儲存實作與替身對錯誤的定義一致,再於組裝應用程式時選擇實作。若改動業務規則,請與保留行為的重構分開處理。

延伸實驗

來源與驗證