コンテンツにスキップ

モックが辛いのはコードの悲鳴 — テスト設計と技術的負債

概要

単体テストでモックが大量に必要になるのは「コードが設計を見直せと叫んでいるサイン」という話をポッドキャスト「くわラジ」#2 で解説。サイクロマティック複雑度や技術的負債の話まで広がっている。

ポッドキャスト: https://kuwa-raji.henteko07.com

詳細

モックが増える根本原因

// BAD: 依存が多くモックだらけのテスト
func (s *UserService) CreateUser(name string) error {
    user := User{Name: name}
    s.db.Save(user)          // → DB モック必要
    s.email.Send(user)       // → メールモック必要
    s.analytics.Track(user)  // → 分析モック必要
    s.cache.Invalidate(user) // → キャッシュモック必要
    return nil
}

上記のテストには 4 つのモックが必要。これはコードが「単一責任原則を破っている」というサイン。

// GOOD: 依存を分離するとモックが減る
func (s *UserService) CreateUser(name string) (User, error) {
    return User{Name: name}, nil  // 純粋関数 → モック不要
}

// 副作用は呼び出し側で組み合わせる
user, _ := service.CreateUser(name)
db.Save(user)
email.Send(user)

サイクロマティック複雑度とモック

サイクロマティック複雑度(分岐の数)が高いコードほど、テストで網羅すべきパスが増え、モックも増える。

複雑度 1-10: テストしやすい
複雑度 11-20: 注意が必要
複雑度 21+: 要リファクタリング

Go での計測ツール: gocyclo, golangci-lint

技術的負債との関係

モック地獄は技術的負債の症状の一つ: - 密結合 → モック多い → テスト維持コスト増 - テスト維持コスト増 → テストを書かなくなる → 品質低下 - 品質低下 → バグ修正に時間 → 新機能開発が遅れる

処方箋

  1. 依存性注入(DI)を徹底する — 外部依存をコンストラクタで受け取る
  2. 副作用を境界に押し出す — ドメインロジックを純粋関数に近づける
  3. インターフェースを薄くする — 必要な操作だけのインターフェースを作る
  4. 統合テストとのバランス — 単体テストでモックするより、実際の DB で統合テストする方が安いケースもある

なぜ重要か / いつ使うか

  • 「テストが書きにくい」と感じたとき、まずコード設計を疑う
  • コードレビューで「モック多すぎ」をリファクタリングの提案根拠にする
  • 技術的負債の優先度付けに「テストの複雑さ」を指標として使う