💊
Goテストで「同じモックを複数回呼ぶ/1回目成功→2回目エラー」をきっちり表現する
ループや再試行ロジックをテストするときに同じメソッドを複数回呼ぶ—「1回目は成功」「2回目はエラー」のように呼び出しごとに結果を切り替えるにはどう書く?
本稿は初学者でも迷わないよう、最小コードで“順序と回数”を保証する実装パターンを3通りで示します。
TL;DR
-
gomock 推奨:
InOrder
で順序を、Times
/DoAndReturn
で回数と挙動を制御。順序制約はデフォルトでは無効なのでInOrder
/Call.After
を使うのがコツ。Go Packages -
testify/mock:
mock.InOrder
(v1.10+)とOnce/Times
の組み合わせが簡潔で分かりやすい。Go Packages - 手書きフェイク:依存ゼロで最速。複雑な順序条件には不向き。
想定シナリオと題材コード
テスト対象は、配列の Item
を順に保存する Process
。2回目の保存で失敗したら処理全体をエラーにする仕様です。
package app
import (
"context"
"fmt"
)
type Item struct{ ID int }
type Repo interface {
Save(ctx context.Context, it Item) error
}
func Process(ctx context.Context, r Repo, items []Item) error {
for i, it := range items {
if err := r.Save(ctx, it); err != nil {
return fmt.Errorf("save %d failed: %w", i+1, err)
}
}
return nil
}
1) gomock:順序+回数+戻り値の切替を“意図が伝わる”形で書く(おすすめ)
gomock は順序は既定では保証されません。InOrder か Call.After を使うと順序制約を付与できます。また Times、DoAndReturn で回数や戻り値を動的に制御。Go Packages
InOrder
で「1回目成功→2回目エラー」の順序を宣言
1-1. package app_test
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
// mockgen で生成したモックを想定
// mockgen -source=repo.go -destination=repo_mock_test.go -package=app_test
// (go:generate に載せておくと便利)
)
func TestProcess_gomock_InOrder(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
m := NewMockRepo(ctrl)
items := []app.Item{{ID: 1}, {ID: 2}}
gomock.InOrder(
m.EXPECT().Save(gomock.Any(), items[0]).Return(nil),
m.EXPECT().Save(gomock.Any(), items[1]).Return(errors.New("boom")),
)
err := app.Process(context.Background(), m, items)
require.Error(t, err)
require.ErrorContains(t, err, "boom")
}
-
InOrder
で呼び出し順の制約を付与(Call.After
でも可。InOrder
のほうが読みやすい)。Go Packages
Times + DoAndReturn
で“同じ期待”に2回ぶんの挙動をまとめる
1-2. func TestProcess_gomock_DoAndReturn(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
m := NewMockRepo(ctrl)
items := []app.Item{{ID: 1}, {ID: 2}}
call := 0
m.EXPECT().
Save(gomock.Any(), gomock.Any()).
Times(2). // ← ちょうど2回
DoAndReturn(func(context.Context, app.Item) error {
call++
if call == 1 {
return nil // 1回目
}
return errors.New("boom") // 2回目
})
err := app.Process(context.Background(), m, items)
require.Error(t, err)
}
-
DoAndReturn
はコール時に実行される関数で戻り値を決められます。Times(n)
と組み合わせると**「n回だけこの挙動」**が表現できます。Go Packages
💡 どちらを使う?
- 明示的に順序を縛りたい →
InOrder
- “同型の呼び出し”を回数でまとめて扱いたい →
Times + DoAndReturn
mock.InOrder
(v1.10+)+ Once/Times
の定石
2) testify/mock:testify/mock
には mock.InOrder
があり、並べた Call
の順序を保証できます。個々の呼び出しは Once
/ Times
で回数を定義し、最後に AssertExpectations
で期待と実績の整合を検証します。Go Packages
package app_test
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type RepoMock struct{ mock.Mock }
func (m *RepoMock) Save(ctx context.Context, it app.Item) error {
args := m.Called(ctx, it)
return args.Error(0)
}
func TestProcess_testify_InOrder(t *testing.T) {
t.Parallel()
m := new(RepoMock)
items := []app.Item{{ID: 1}, {ID: 2}}
c1 := m.On("Save", mock.Anything, items[0]).Return(nil).Once()
c2 := m.On("Save", mock.Anything, items[1]).Return(errors.New("boom")).Once()
mock.InOrder(c1, c2) // ← 順序保証(v1.10+)
err := app.Process(context.Background(), m, items)
require.Error(t, err)
m.AssertExpectations(t) // 回数・未消化の期待を確認
}
-
mock.InOrder
は v1.10.0 で追加。Once/Times
などの回数指定 API と併用します。Go Packages
3) 依存ゼロの「手書きフェイク」:小規模には最速
外部ライブラリなしで、状態を持つフェイクを1つ置くだけでも目的は達せられます。
type FakeRepo struct{ calls int }
func (f *FakeRepo) Save(ctx context.Context, it app.Item) error {
f.calls++
if f.calls == 1 {
return nil
}
return errors.New("boom")
}
func TestProcess_fake(t *testing.T) {
t.Parallel()
f := &FakeRepo{}
items := []app.Item{{1}, {2}}
err := app.Process(context.Background(), f, items)
require.Error(t, err)
}
Pros: 依存ゼロ/読みやすい
Cons: メソッドが増える・順序条件が複雑になるほどメンテがつらくなる → gomock/testify に移行推奨
よくある落とし穴(と回避策)
-
順序がすり抜ける
- gomock は既定で順序を強制しないため、必要なら
InOrder
かCall.After
を使います。Go Packages - testify/mock は
mock.InOrder
(v1.10+)を使い、Once/Times
と組み合わせます。Go Packages
- gomock は既定で順序を強制しないため、必要なら
-
回数がずれる
- gomock:
Times(n)
/AnyTimes()
を正しく指定。Finish()
はdefer
かt.Cleanup
で必ず呼ぶ。Go Packages - testify/mock:最後に
AssertExpectations(t)
で未実行の期待を検出。Go Packages
- gomock:
-
引数比較でコケる
- gomock:
gomock.Any()
/AssignableToTypeOf
などのマッチャを使う。Go Packages - testify/mock:
mock.Anything
/MatchedBy
を活用。Go Packages
- gomock:
セットアップ一式(コピペ用)
gomock 導入とモック生成
go get go.uber.org/mock@latest
go install go.uber.org/mock/mockgen@latest
# 例: インターフェイスからモック生成
mockgen -source=repo.go -destination=repo_mock_test.go -package=app_test
-
InOrder
/Call.After
/DoAndReturn
/Times
は gomock 本体に含まれる API(go.uber.org/mock/gomock
)。Go Packages - Uber によるフォークでメンテ継続中(背景)。GitHub
testify/mock の導入
go get github.com/stretchr/testify@latest
-
mock.InOrder
(v1.10+)、Once
、Times
、AssertExpectations
を提供。Go Packages
使い分け指針(短評)
-
gomock を常用するチーム
-
InOrder
で順序を、Times/DoAndReturn
で回数・動的戻り値を制御。意図が一目で伝わる。Go Packages
-
-
testify を既に使っている
-
mock.InOrder
+Once/Times
で十分シンプル。require
/assert
群とも親和性◎。Go Packages
-
-
最小依存で済ませたい小テスト
- 手書きフェイクで OK(増えてきたらモックへ移行)。
付録:多段シナリオのヒント
-
A→B→A のようなクロス順序:
- gomock なら
Call.After
を組み合わせて任意依存関係を作れます。InOrder
より柔軟。Go Packages
- gomock なら
-
3回以上で段階的に結果を変える:
- gomock:
Times(3)
+DoAndReturn
内でカウンタ分岐。Go Packages - testify:
On(...).Return(...).Times(n)
を並べるか、Run
/MatchedBy
を併用。Go Packages
- gomock:
参考資料
- gomock(
go.uber.org/mock/gomock
)公式ドキュメント:InOrder / Call.After / DoAndReturn / Times の解説あり。Go Packages - testify/mock 公式ドキュメント:mock.InOrder(v1.10+)/ Once / Times / AssertExpectations。Go Packages
- gomock(Uber フォーク)リポジトリの背景説明。GitHub
Discussion