モックと依存注入のレビュー:過剰抽象か適切分離かをどう判断するか

Go言語では、テスト容易性を高めるためにinterfaceによる抽象化とモック化が頻繁に行われる。
しかしレビューの現場では、その抽象が本当に必要か?、または抽象過剰による複雑化を招いていないか?という観点が重要になる。

1. モックとDIの目的整理

モックとDIとは

モックとは、実装を差し替えるためのスタブ(擬似実装)
DI(依存注入)とは、構造体の内部で依存先を直接生成せず、外部から注入して責務を分離する設計

主な目的:

  • テスト容易性の向上(単体テストを外部依存なく実行)
  • 実装の柔軟性(後から差し替えや拡張がしやすい)
  • 責務分離(呼び出す側は「何をするか」だけに集中)

2. 典型的なモック化構造

interfaceによる依存抽象
type MailSender interface {
    SendMail(to, subject, body string) error
}
type RealMailSender struct{}
func (r *RealMailSender) SendMail(to, subject, body string) error {
    // SMTP送信処理
    return nil
}
type UserService struct {
    sender MailSender
}
func (s *UserService) NotifyUser(email string) error {
    return s.sender.SendMail(email, "Hello", "Welcome!")
}
Comment
@Reviewer: `UserService` は `MailSender` に依存しているが、直接の型ではなくインターフェースで抽象化されているため、テスト時には差し替え可能な設計。  
モックを使って `NotifyUser()` の動作だけを検証可能になっている点を評価する。

3. モックが不要な場面での“抽象の過剰”

以下は過剰抽象の典型パターン:

過剰なinterface分離
type TimeProvider interface {
    Now() time.Time
}
Comment
@Reviewer: `time.Now()` という組み込みの静的関数をラップするだけのインターフェースは過剰抽象の典型。  
このラップによりテスト可能性は得られるが、コード全体の冗長さ・読解難度・初見の理解コストを高める。  
頻繁に変化しない、かつ単純な依存(日時、UUIDなど)は直接使用を原則とし、例外を設計意図と共に明示させる。

4. 適切なDIと過剰抽象の構造比較

UML Diagram
Comment
@Reviewer: この構造ではUserServiceのテストがMockMailSenderで完結可能。  
設計の抽象は「将来変わる可能性がある依存」だけに限定すべきで、恒久的な依存は具象型でよいケースもある。

5. レビュー観点:抽象の妥当性

観点 確認事項
責務分離 インターフェース化は呼び出し側の責務単純化に寄与しているか?
実装の切り替え 将来の差し替えや条件分岐での柔軟性が想定されているか?
テスト目的 モック化によってテストの粒度が適切に絞られているか?
過剰抽象 ラップだけの無意味なinterfaceではないか?実装が1種類しかないならinterface不要では?
実体の注入元 DIの注入方法(構造体の初期化、Factory、Injectorなど)が妥当か?

6. 適切なモック実装とテスト

モック構造のテスト使用例
type MockMailSender struct {
    Sent bool
}
func (m *MockMailSender) SendMail(to, subject, body string) error {
    m.Sent = true
    return nil
}
func TestNotifyUser(t *testing.T) {
    mock := &MockMailSender{}
    service := UserService{sender: mock}
    _ = service.NotifyUser("[email protected]")
    if !mock.Sent {
        t.Errorf("メール送信が呼び出されていません")
    }
}
Comment
@Reviewer: このように、モックは「副作用が発生したか」の確認が主目的。  
一方、戻り値・副作用が無いメソッドに対するモックは意義が薄く、テスト実装コストが上回る場合も多い。

7. 適切な設計判断の指摘例

- type UUIDProvider interface {
-     NewUUID() string
- }
+ // 組み込みuuid.New().String() で十分
Comment
@Reviewer: UUIDのように非可変かつ意味が固定された生成関数に対し、interfaceを挟む意義は薄い。  
変更頻度や切り替えニーズが将来的に見込まれないなら、具象関数をそのまま使う方がシンプルかつ明快。

8. まとめ:抽象の「目的」と「将来性」が見えているか

レビューでは、interface抽象やモック設計が「未来を見越した柔軟性」に根ざしているか、
それとも単に「テストのための構文的都合」で導入されているだけかを見極める必要がある。

設計者に以下を問うことが有効:

  • このinterfaceは将来の切り替えや条件変更を想定しているか?
  • 具象実装が1種類だけで今後も変わらない見込みなら、あえて抽象にする意味は?
  • テスト容易性が設計の主目的になっていないか?それは本当に全体設計にとって価値があるか?

これらを通じて、レビューアーとして設計の「現実性」と「将来性」のバランスを冷静に評価していくことが求められる。