0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】TDD Red フェーズで mock nil → `panic: nil pointer dereference` の対処法

0
Posted at

エラーメッセージ

--- 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 ガードにより:

  1. Red: assert.ErrorIs(t, err, ErrValidation) が失敗する(errErrValidation ではない)— panic せず、返却された err から「リポジトリへの想定外の呼び出し」が分かる
  2. Green: UseCase にバリデーションを追加 -- リポジトリに到達せず ErrValidation を返す
  3. Refactor: 必要に応じて整理

挙動の対照(注釈)

フェーズ / 設定 uc.Create が到達する先 テストの見え方
Red・モック ガードなし repo.CreateCreateFuncnilpanic ErrorIs まで行かず panic 失敗
Red・モック ガードあり repo.Createerror を返す(想定外呼び出し) 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 ガードを入れるか、テスト側で必ずスタブを渡すかはチームの方針次第。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?