ユニットテストと実装の乖離レビュー完全ガイド:カバレッジ依存しない設計整合性の評価手法

ユニットテストのレビューでは、「テストが存在するか」ではなく「実装責務を適切に検証しているか」が重要な評価軸になる。
カバレッジ100%でも設計的に破綻したテストは多く、レビューアーは構造網羅的な整合性視点を養う必要がある。

本稿では、レビューアーがテストコードを設計意図の反映度で評価するための実践的レビュー観点を整理する。


良い実装例:実装責務とテスト構造の整合性が取れた設計

type Notifier interface {
  Send(msg string) error
}

func ValidateAndNotify(v int, notifier Notifier) error {
  if v < 0 {
    return errors.New("invalid")
  }
  return notifier.Send(fmt.Sprintf("value: %d", v))
}
func TestValidateAndNotify(t *testing.T) {
  cases := []struct {
    name     string
    input    int
    wantErr  bool
    notifyOk bool
  }{
    {"負の値", -1, true, false},
    {"正常値", 10, false, true},
  }

  for _, tt := range cases {
    tt := tt
    t.Run(tt.name, func(t *testing.T) {
      dummy := &mockNotifier{}
      err := ValidateAndNotify(tt.input, dummy)
      if tt.wantErr && err == nil {
        t.Errorf("expected error, got nil")
      }
      if tt.notifyOk && !dummy.Called {
        t.Errorf("notifier was not called")
      }
    })
  }
}
  • 実装が持つ責務(検証・通知)が両方網羅
  • 条件分岐ごとに個別ケース化
  • 副作用(notifier呼出し)も明示的検証

レビューアーが読み解きやすい「設計意図=テスト構造」の理想系。


問題のある実装例とレビュー指摘

① 正常系のみの部分テスト(分岐網羅不足)

func Check(v int) string {
  if v > 0 {
    return "positive"
  }
  return "non-positive"
}

func TestCheck(t *testing.T) {
  if Check(1) != "positive" {
    t.Fail()
  }
}
@Reviewer
負の値やゼロ経路が未検証です。全分岐条件を個別に用意し、構造網羅を満たしてください。

カバレッジ100%でも「設計意図カバー率」は0%──レビューで常に区別すべき概念。


② 実装責務がテスト対象外(副作用未検証)

func ValidateAndNotify(v int, notifier Notifier) error {
  if v < 0 {
    return errors.New("invalid")
  }
  return notifier.Send(fmt.Sprintf("value: %d", v))
}

func TestValidateAndNotify(t *testing.T) {
  _ = ValidateAndNotify(10, dummy)
}
@Reviewer
notifer.Send呼出し結果が検証対象外です。副作用責務を含めテスト内でmock検証を追加してください。

「呼ばれたかどうか」「引数が正しいか」まで含めた副作用検証を怠ると責務崩壊する。


③ テスト粒度が内部ロジック寄りに偏重

func Compute(v int) (int, error) {
  if v < 0 {
    return 0, errors.New("invalid")
  }
  return v * 10, nil
}

func TestComputeNegativeOnly(t *testing.T) {
  _, err := Compute(-1)
  if err == nil {
    t.Fail()
  }
}
@Reviewer
責務範囲の一部のみを切り出したテストになっています。全入力空間に対する正常系・異常系を整理し網羅してください。

レビューでは「何を切り出してテストしているのか?」の意図不一致を最優先で確認すること。


④ 外部依存をmock化せず実接続

func TestDB(t *testing.T) {
  conn := ConnectDB() // 実DB接続
  result := conn.Query("SELECT ...")
  ...
}
@Reviewer
実DBを使用するユニットテストは設計的に破綻しています。mockやインメモリ代替で責務を純粋化してください。

「ユニットテストに実サービス依存」=実質結合テスト化。
レビューでは最も強い是正対象。


実装構造 vs テスト構造乖離フロー図

UML Diagram

実務レビューで重点的に観察すべき5領域

項目 内容 チェック意図
分岐網羅 if / switch / エラーハンドリング 正常系・異常系対称性
副作用検証 DB, API, IO 呼出し発生・結果内容
責務一致性 実装API単位 ↔ テスト単位 粒度不整合有無
境界網羅 0件・空文字・限界値 実装限界検証
構造崩壊防止 mock未使用・隠れ結合 ユニット純度確認

事故事例①:正常系のみの設計乖離

func Register(user User) error {
  if user.Name == "" {
    return errors.New("empty name")
  }
  return db.Save(user)
}

func TestRegister(t *testing.T) {
  err := Register(User{Name: "Taro"})
  if err != nil {
    t.Fail()
  }
}
@Reviewer
入力バリデーション責務に対応する異常系ケースが不足しています。空文字ケースを追加してください。

事故事例②:副作用未検証による責務隠蔽

func SendNotification(msg string, sender Sender) error {
  return sender.Send(msg)
}

func TestSendNotification(t *testing.T) {
  _ = SendNotification("hello", dummy)
}
@Reviewer
sender.Send呼出しそのものがテスト検証対象外になっています。mockで呼出し回数・引数一致確認を実装してください。

事故事例③:構造肥大化に伴う責務崩壊

func ProcessOrder(user User, product Product, payment Payment) error {
  if !CheckProductStock(product) {
    return errors.New("stock empty")
  }
  err := Pay(payment)
  if err != nil {
    return err
  }
  return SaveOrder(user, product)
}

func TestProcessOrder(t *testing.T) {
  err := ProcessOrder(user, product, payment)
  if err != nil {
    t.Fail()
  }
}
@Reviewer
複数責務が混在しており、それぞれの分岐ケース検証が不足しています。処理単位で責務分割と個別テストを整理してください。

テスト設計レビューは「設計モデル可視化力」の訓練場

レビューアーは、実装コードとテストコードを「責務マッピング表」として頭の中に描く意識が重要。

  • 実装が持つ責務群を抽出 →
  • 各責務に対してテストケース網羅確認 →
  • 対応の有無を読み取る → 是正指摘

レビュー実施フロー総まとめ

UML Diagram