テストとコード品質

シングルトンの依存を注入して保存の成功と失敗をテストする

保存先のプロトコルと記録用テストダブルを使い、注文の引数、書き込み前の入力拒否、保存エラーの伝播をDBなしで検証します。

読了 3 分
シングルトンの依存を注入して保存の成功と失敗をテストするのカバー画像

問題はインスタンスが一つであることだけでなく、呼び出し側が具体的な保存先を直接選ぶことです。役割をプロトコルで表し、実装を注入すれば、ネットワークなしでサービスの判断を検証できます。

仕組みと設計の判断基準

外部からオブジェクトを受け取るだけでは、別実装に差し替えられるとは限りません。具体的なDatabase型の引数なら、テストダブルにもその型制約が残ります。ここではOrderStoreという小さな契約を定義し、any OrderStoreを受け取ります。契約を満たせばシングルトンも注入できます。 一次資料

再現コード

注文、エラー、プロトコル、サービス、記録用保存先をすべて定義しました。成功時の注文の値、空のIDを保存前に拒否すること、保存エラーを呼び出し元へ伝えることを検証します。ダブルは記録するspyと失敗を設定するstubを兼ねています。

この独立したサンプルは2026-10-23に、arm64 macOS向けのApple Swift 6.4をSwift 6言語モードで実行しました。ネットワーク接続や追加パッケージは不要です。

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では保存呼び出しが増えず、保存エラーも握りつぶされませんでした。単なる回数だけでなく、引数の値と二つの失敗経路を確認しています。

適用範囲と注意点

プロトコルを使っても、実DBのトランザクション、一意制約、ネットワーク動作は検証できません。それらには結合テストが必要です。すべてを抽象化する必要はなく、共有グローバル状態を注入してもテストの独立性は自動で得られません。

次に確認すること

外部I/O境界の一か所から小さな契約を作ります。本番実装とダブルでエラーの意味が一致するか確認し、アプリを組み立てる場所で実装を選びます。業務ルール変更は動作を保つリファクタリングと分けてください。

関連する実験

出典と確認基準