問題はインスタンスが一つであることだけでなく、呼び出し側が具体的な保存先を直接選ぶことです。役割をプロトコルで表し、実装を注入すれば、ネットワークなしでサービスの判断を検証できます。
仕組みと設計の判断基準
外部からオブジェクトを受け取るだけでは、別実装に差し替えられるとは限りません。具体的なDatabase型の引数なら、テストダブルにもその型制約が残ります。ここではOrderStoreという小さな契約を定義し、any OrderStoreを受け取ります。契約を満たせばシングルトンも注入できます。 一次資料
再現コード
注文、エラー、プロトコル、サービス、記録用保存先をすべて定義しました。成功時の注文の値、空のIDを保存前に拒否すること、保存エラーを呼び出し元へ伝えることを検証します。ダブルは記録するspyと失敗を設定するstubを兼ねています。
この独立したサンプルは2026-10-23に、arm64 macOS向けのApple Swift 6.4をSwift 6言語モードで実行しました。ネットワーク接続や追加パッケージは不要です。
ダウンロード先のフォルダーで実行してください。検証条件を満たさない場合は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境界の一か所から小さな契約を作ります。本番実装とダブルでエラーの意味が一致するか確認し、アプリを組み立てる場所で実装を選びます。業務ルール変更は動作を保つリファクタリングと分けてください。
出典と確認基準
- Inversion of Control Containers and the Dependency Injection PatternMartin Fowler · 著者の原文 · 確認日 2026年10月23日根拠: 実装の契約と説明の根拠です。出力は、この記事に掲載した実験で別途確認しました。

