エラーメッセージ
--- FAIL: TestCreateItem_ValidationError (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
panic: runtime error: invalid memory address or nil pointer dereference
環境
| 項目 | バージョン |
|---|---|
| Go | 1.22 |
状況
TDD の Red フェーズでバリデーションエラーのテストを書いた。「バリデーションで弾かれるから UseCase 内のリポジトリ呼び出しには到達しない」と判断し、モックのメソッド関数を設定しなかった。
**テストの意図(あとから実装する世界)**では、Name == "" の時点でユースケースが ErrValidation を返し、リポジトリの Create は呼ばれない。
Red 時点の実装ではバリデーションがまだ無いため、Create は いきなり repo.Create まで進む。ここでモックの CreateFunc が未設定(nil)だと、次節のとおり panic になる。
func TestCreateItem_ValidationError(t *testing.T) {
ctx := context.Background()
uc := NewItemUseCase(&MockItemRepo{
// CreateFunc を設定していない(nil のまま)
})
err := uc.Create(ctx, &Item{Name: ""}) // 空文字でバリデーションエラーを期待
assert.ErrorIs(t, err, ErrValidation)
}
原因
Red フェーズではまだバリデーションが実装されていない。UseCase のメソッドは素通りしてリポジトリの Create を呼び出し、nil の関数ポインタをコールして panic する。
ガードなしのとき、uc.Create の内部で panic するため、通常は err := ... の代入のあとまで進めず、assert.ErrorIs 自体は実行されない(テストは「panic で失敗」として終わる)。「ErrValidation ではないので赤い」ではなく、「そもそもアサーションまで行っていない」点が混乱しやすい。
// Red フェーズ時点の UseCase(バリデーション未実装)
func (uc *ItemUseCase) Create(ctx context.Context, item *Item) error {
// ここにバリデーションが入る予定だが、Red ではまだない
return uc.repo.Create(ctx, item) // mock の CreateFunc が nil → panic
}
// モック
type MockItemRepo struct {
CreateFunc func(ctx context.Context, item *Item) error
}
func (m *MockItemRepo) Create(ctx context.Context, item *Item) error {
return m.CreateFunc(ctx, item) // CreateFunc が nil → panic
}
解決方法
モックのメソッドに nil ガードを追加し、未設定の場合は panic ではなく安全にエラーを返す。
func (m *MockItemRepo) Create(ctx context.Context, item *Item) error {
if m.CreateFunc == nil {
return fmt.Errorf("unexpected call to Create")
}
return m.CreateFunc(ctx, item)
}
Red フェーズでは「まだ実装がない」ことが前提。モックへの到達を防ぐバリデーションもまだ存在しないため、モック側で nil 呼び出しを安全に扱うか、あるいは Red の時点でも CreateFunc にスタブを入れて panic を避ける、といった対処がよくある。
この nil ガードにより:
-
Red:
assert.ErrorIs(t, err, ErrValidation)が失敗する(errがErrValidationではない)— panic せず、返却されたerrから「リポジトリへの想定外の呼び出し」が分かる -
Green: UseCase にバリデーションを追加 -- リポジトリに到達せず
ErrValidationを返す - Refactor: 必要に応じて整理
挙動の対照(注釈)
| フェーズ / 設定 |
uc.Create が到達する先 |
テストの見え方 |
|---|---|---|
| Red・モック ガードなし |
repo.Create → CreateFunc が nil で panic
|
ErrorIs まで行かず panic 失敗 |
| Red・モック ガードあり |
repo.Create が error を返す(想定外呼び出し) |
ErrorIs は実行されるが ErrValidation でない と失敗(意図した Red) |
| Green(バリデーション実装後) |
repo.Create に到達しない
|
ErrorIs(..., ErrValidation) が 成功
|
同じ「まだバリデーションが無い Red」でも、ガードの有無で失敗の仕方(panic か、アサーション失敗か)が変わる、と押さえると読みやすい。
モックのヘルパーパターン
プロジェクト全体で統一するなら、テストダブル生成ツール(vektra/mockery 等)や、「メソッドを未設定のまま呼んだときにどうするか」(panic にする/既定のエラーを返す、など)という振る舞いをテンプレや生成コードで揃える方法もある。メソッドごとに手書きするなら、次のような汎用ヘルパーも検討の余地はある(ただし T の制約や reflect の扱いは用途に合わせる必要がある)。
func safeMock[T any](fn T) T {
// 例: 関数型なら reflect.ValueOf(&fn).Elem().IsNil() などで nil 判定し、
// 未設定ならデフォルトのエラー返却関数を差し込む、といった案
// ...プロジェクトの規模・生成方法に応じて検討
}
Red で panic するのは「実装がまだ追いついていない」シグナルにはなるが、assert の失敗としての Red(期待どおりのエラーかを検証する形)とは別物で、スタックトレース中心だと原因が読みにくいことも多い。nil ガードを入れるか、テスト側で必ずスタブを渡すかはチームの方針次第。