ユニットテストと実装の乖離レビュー完全ガイド:カバレッジ依存しない設計整合性の評価手法
ユニットテストと実装の乖離レビュー完全ガイド:カバレッジ依存しない設計整合性の評価手法
ユニットテストのレビューでは、「テストが存在するか」ではなく「実装責務を適切に検証しているか」が重要な評価軸になる。
カバレッジ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)
}
@Reviewernotifer.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 テスト構造乖離フロー図
実務レビューで重点的に観察すべき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)
}
@Reviewersender.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複数責務が混在しており、それぞれの分岐ケース検証が不足しています。処理単位で責務分割と個別テストを整理してください。
テスト設計レビューは「設計モデル可視化力」の訓練場
レビューアーは、実装コードとテストコードを「責務マッピング表」として頭の中に描く意識が重要。
- 実装が持つ責務群を抽出 →
- 各責務に対してテストケース網羅確認 →
- 対応の有無を読み取る → 是正指摘