モックが多すぎるのはコードが設計見直しを叫んでいるサイン
概要¶
単体テストでモックが大量に必要になるとき、それはコードの設計に問題があることを示している。「くわラジ」#2 でモック地獄から始まり、サイクロマティック複雑度・技術的負債の話まで展開したポッドキャスト。
詳細¶
「モック地獄」とは¶
// モックが多すぎるテストの例
func TestOrderService_PlaceOrder(t *testing.T) {
mockDB := NewMockDatabase()
mockEmail := NewMockEmailSender()
mockInventory := NewMockInventoryService()
mockPayment := NewMockPaymentGateway()
mockLogger := NewMockLogger()
mockMetrics := NewMockMetricsCollector()
mockCache := NewMockCache()
// まだ続く...
svc := NewOrderService(mockDB, mockEmail, mockInventory,
mockPayment, mockLogger, mockMetrics, mockCache)
svc.PlaceOrder(...)
}
モックが7個も必要になっている → このクラスは依存が多すぎる = 責務が多すぎる
なぜモックが多いと設計が悪いのか¶
モックが必要な理由 = 外部に依存しているから
依存が多い = 関心事が多い = 単一責任の原則違反
単一責任の原則:
「クラスは変更される理由が1つだけであるべき」
→ 依存先が多いということは変更理由が多いということ
設計の問題を見つけるシグナル¶
| シグナル | 意味 |
|---|---|
| テストのセットアップが長い | クラスの依存が多い |
| 全てのメソッドで同じモックが必要 | 依存を注入する場所が間違っている |
| モックの動作設定が複雑 | ロジックがサービス層に漏れている |
| モックを変えるとテストが大量に落ちる | 抽象化が薄い(実装に依存しすぎ) |
サイクロマティック複雑度との関係¶
サイクロマティック複雑度 = コード内の独立した実行パスの数
if 1つ = +1
for 1つ = +1
case 1つ = +1
複雑度が高い = テストケースが多い = モックも多くなりがち
目安:
1〜5: シンプル
6〜10: やや複雑、注意
11〜: 複雑すぎ、リファクタリングを検討
解決アプローチ¶
1. 依存を減らす(責務を分割する)¶
// Before: OrderService が全てを知っている
type OrderService struct {
db Database
email EmailSender
inventory InventoryService
payment PaymentGateway
// ...
}
// After: 各責務を小さなサービスに分割
type OrderPlacer struct {
inventory InventoryChecker // この責務だけ
payment PaymentProcessor // この責務だけ
}
type OrderNotifier struct {
email EmailSender // 通知責務だけ
}
2. ポートとアダプターパターン(Hexagonal Architecture)¶
// ポート(インターフェース)だけに依存する
type OrderRepository interface {
Save(order Order) error
FindByID(id string) (Order, error)
}
// 実装(アダプター)はテスト時にインメモリで差し替え可能
type InMemoryOrderRepository struct {
orders map[string]Order
}
3. 統合テストで補う¶
全てを単体テストしようとするとモックだらけになる。 依存先が多い部分はむしろ 統合テスト に任せる。
技術的負債との関係¶
モック地獄は技術的負債が可視化されたもの。 テストを書くことで「設計の問題」が浮き彫りになる → テストは設計のフィードバックツール。
なぜ重要か / いつ使うか¶
- テストのセットアップに時間がかかりすぎるとき
- 新機能を追加するたびに既存テストが大量に落ちるとき
- 「テストを書けと言われるがどう書けばいいか」と感じるとき