Singletonのせいでテストが書けず、一晩中悩んだことはありませんか?
ロジックは問題ないのに、テストを追加しようとすると実DBを叩き、実APIを呼び出してしまいます。
結論からお話しします。
Singletonはコード内でオブジェクトを直接取得するため、テスト時にフェイクへ差し替えられません。
一方、依存性注入は必要なオブジェクトを外から渡す方式なので、テストで自由に差し替えられます。
今日は、実際のコードを使ってこの2つの違いを順に見ていきます。
なぜSingletonはテストを妨げるのでしょうか?
Singletonは、プログラム全体に1つだけ存在するオブジェクトです。
便利ではあります。どこからでもDatabase.sharedの1行で取得できますから。
問題はここから始まります。
関数内でオブジェクトを直接取得すると、その関数は実際のデータベースに強く結合されます。
テスト時に「本物ではなくフェイクDBを使って」と指定する方法がありません。
// 関数内でSingletonを直接取得
func saveOrder(_ order: Order) {
// 実際の DBに強く結合される
let db = Database.shared
db.insert(order) // テストでこれを防ぐ方法がない
}
このコードをテストするには、実DBが起動していなければなりません。
DBが停止していれば、テストも失敗します。
ロジックは間違っていないのに、インフラのせいでテストが赤くなるのです。
依存性注入はどう使うのでしょうか?
依存性注入は、思ったほど大げさなものではありません。
関数内でオブジェクトを取得せず、外で作って渡すだけです。
文字どおり、必要なものを「注入」するのです。
// DBパラメーターとして外から受け取る
func saveOrder(_ order: Order, db: Database) {
db.insert(order) // どの DBにするかは呼び出し側が決める
}
違いはたった1行ですよね?
Database.sharedで直接取得していたものを、パラメーターで受け取るように変えただけです。
しかし、この小さな変更でテストは大きく変わります。
実コードでは実DBを、テストではフェイクDBを渡せばよいからです。
イニシャライザーで受け取る方式もよく使います。
class OrderService {
private let db: Database
// オブジェクト生成時に外から受け取って保持する
init(db: Database) { self.db = db }
}
一度受け取れば、クラス内のどこからでもself.dbとして使えるので、よりすっきりします。
テストでフェイクオブジェクトに差し替える
ここからが本当に面白いところです。
依存性を外から受け取るようにしたので、テストでは実DBの代わりにフェイクDB(通常モックと呼びます)を渡せます。
@Test func 注文を_保存すると、_DBに_渡される() {
let fake = FakeDatabase() // 本物ではなくフェイク
saveOrder(Order(), db: fake)
#expect(fake.savedCount == 1) // 保存が呼ばれたことだけ確認
}
実DBを起動する必要はありません。
ネットワークも外部サーバーも不要です。メモリー上ですぐに終わります。
そのおかげでテストは速くなり、何より安定します。
外部の状況に左右されず、ロジックだけを正確に検証できるからです。
Singletonと依存性注入を一目で比較
2つの違いを表にまとめました。
| 区分 | Singletonを直接使用 | 依存性注入 |
|---|---|---|
| オブジェクトを取得する場所 | 関数内で直接取得 | 外から渡す |
| フェイクオブジェクトへの差し替え | 事実上不可能 | 自由に可能 |
| テスト速度 | 遅い(実リソースが必要) | 速い(メモリーで処理) |
| テストの安定性 | 外部状況に左右される | ロジックだけ検証可能 |
| 結合度 | 高い | 低い |
Singleton自体が必ずしも悪いわけではありません。
設定値のように、本当に1つだけあればよい場合には便利です。
ただし、「どこからでも直接取得する」習慣が問題です。それがテストの手足を縛ります。
よくある質問(Q&A)
Q. 依存性注入には、SwinjectのようなDIライブラリが必須ですか?
いいえ。今日のコードのようにパラメーターやイニシャライザーで渡すだけでも、すでに依存性注入です。ライブラリはこれを自動管理するだけです。
Q. パラメーターが増えすぎると、複雑になりませんか?
そのとおりです。そこで通常はイニシャライザー注入でまとめて受け取るか、関連するものをまとめます。パラメーターが増え続けるなら、そのクラスが多くの仕事を抱えすぎているサインでもあります。
テストが実リソースに足を引っ張られるなら、今日の話に沿って1か所だけ変えてみてください。
オブジェクトを直接取得するコードを、外から受け取る形に変えるのです。
その小さな習慣が、テストを書きたくさせてくれます。今日も快適なコーディングを!

