テストとコード品質

Singletonがテストを妨げる理由を、依存性注入で解決してみました

Singletonのせいでテストが書けず、一晩中悩んだことはありませんか?

読了 4 分
Singletonがテストを妨げる理由を、依存性注入で解決してみましたのカバー画像

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. パラメーターが増えすぎると、複雑になりませんか?

そのとおりです。そこで通常はイニシャライザー注入でまとめて受け取るか、関連するものをまとめます。パラメーターが増え続けるなら、そのクラスが多くの仕事を抱えすぎているサインでもあります。

本物の代わりにフェイクDBを差し込むこと。これが核心です
本物の代わりにフェイクDBを差し込むこと。これが核心です

テストが実リソースに足を引っ張られるなら、今日の話に沿って1か所だけ変えてみてください。

オブジェクトを直接取得するコードを、外から受け取る形に変えるのです。

その小さな習慣が、テストを書きたくさせてくれます。今日も快適なコーディングを!

あわせて読みたい