gomock の InOrder に挫折!?1回目と2回目の返り値を変える方法とその裏技
1. gomock で実行順序を制御する方法
1-1. gomock.InOrder() の基本
GoMockには、メソッド呼び出し順序を厳密にテストしたいときのために InOrder(...) という関数が用意されています。具体的には、
gomock.InOrder(
mockObj.EXPECT().MethodA().Return(...),
mockObj.EXPECT().MethodB().Return(...),
)
という形で宣言すると、「MethodA → MethodB の順番で呼ばれる」ことを担保できるわけです。この記述を行わない場合は、MethodA と MethodB がどちらから呼ばれてもテスト上は問題ない(順序は検証しない)ことになります。
なぜ順序を明示的に指定するのか?
-
シナリオテストを組み立てやすい
例えば「ユーザーの認証が成功したらデータ取得を行う」といったケースでは、「先に認証メソッド、次にデータ取得メソッド」という順序の正しさをテストしたいことがあります。 - 想定以外の呼び出し順序をエラーとできる
順序が崩れていた場合、テストが失敗して原因を早期に特定できるため、バグ混入を防ぎやすくなります。
1-2. InOrder でよくある呼び出し順序制御の例
たとえば、2回呼ばれる同じメソッドに異なる返り値を設定したいケースを考えてみましょう。単純化した例で書くと以下のようになります。
gomock.InOrder(
mockObj.EXPECT().
DoSomething().
Return("FirstResult").
Times(1),
mockObj.EXPECT().
DoSomething().
Return("SecondResult").
Times(1),
)
これによって、1回目の DoSomething()
呼び出しで "FirstResult" が返り、2回目の呼び出しで "SecondResult" が返ること を順序込みで保証できます。
1-3. InOrder を使うときの利点・欠点
利点
- テスト意図が明確になる
「このメソッドがこの順序で呼ばれるべき」という仕様を可視化できるため、他の開発者が見ても分かりやすい。 - 予期しない呼び出し順序に早く気づける
仕様変更やリファクタリングで順番が入れ替わるとテストが即座に失敗するので、デバッグがしやすい。
欠点
- 柔軟性が下がる
何らかの理由でメソッドの呼び出し順が変わると「それもアリ」な場面であってもテストが落ちるため、テストコードが過度に順序に依存してしまう可能性がある。 - 複雑な制御には書きづらい場合がある
すべての呼び出し順序を1本のInOrder(...)
に詰め込むと読みにくくなり、テストコード自体の可読性が落ちることも。
「順序を部分的に制御したい」「条件分岐によっては呼び出し順が変化する」というようにロジックが入り組む場合は、他の手段が必要になるケースがあります。
1-4. InOrder がうまく動かないときの次のステップ
InOrder はモック呼び出しの順序を指定する強力な手法ですが、 場合によっては予想外の動きをしたり、想定と違う場所でテストが落ちたり することがあります。具体的には以下のような状況です。
- テスト対象コードの実行フローが並行/非同期を含んでいる
goroutine 内で呼び出されるメソッドなどは、実行タイミングが読みにくく、順序保証が難しくなることがあります。 - 同じメソッド呼び出しが多段階にわたる
例: 1回の関数呼び出しの中で複数回Greet()
を呼んでいるようなケース。すべてに対して順序指定をしていると、ちょっとした修正でテストが大量に壊れてしまう恐れがある。
こうしたケースでは、 InOrder にこだわりすぎず、「呼び出し回数や返り値切り替えを自力で制御したい」 という場面が出てきます。そこを解決するのが次の章で解説する DoAndReturn
を使ったアプローチです。
2. どうしてもうまくいかないときに使う DoAndReturn
2-1. DoAndReturn とは?
GoMock の DoAndReturn
は、 モックメソッド呼び出し時の挙動を動的に制御する ための仕組みです。Return(...)
のように単純な静的返り値を指定する代わりに、 関数(クロージャ)を渡して、その場で計算した結果 を返すことができます。
たとえば、以下のような記述をイメージしてください。
mockObj.EXPECT().
SomeMethod().
DoAndReturn(func(input string) string {
// ここで受け取った引数をもとに何か処理し、
// その結果を返すことができる
return "DynamicResult"
})
このように、 呼び出しのたびに好きなロジックを実行し、その結果を返せる のが DoAndReturn
の強みです。
2-2. 「1回目はこれ、2回目はあれ」を実現するテクニック
InOrder では「順序」を明示しますが、「 同じメソッドを複数回呼ぶ 」パターンだと想定以上に書きづらいときがあります。そこで DoAndReturn
を使うと、 呼び出し回数に応じて返り値を切り替える という手法が可能になります。具体的には、以下のような書き方です。
callCount := 0
mockObj.EXPECT().
SomeMethod().
Times(2). // 2回呼ばれる想定
DoAndReturn(func() string {
callCount++
if callCount == 1 {
return "FirstCall"
}
return "SecondCall"
})
- 解説
-
callCount
というカウンタ変数を用意し、クロージャの中で 1回目の呼び出しか、2回目の呼び出しか を判定しています。 - 1回目は
"FirstCall"
を返し、2回目は"SecondCall"
を返すようにすることで、「呼び出すごとに異なる返り値を返す」 という動作をエレガントに実装できます。 - InOrder を使わずとも、「1回目→これ」「2回目→あれ」というシナリオをスッキリ書けるのがメリットです。
2-3. DoAndReturn が役立つケース
- 複雑なフローの一部で返り値を変えたい
- 同じメソッドを繰り返し呼ぶが、呼び出し回数や引数によって返り値を変えたいときに便利です。
- 例:「初回はネットワークエラーを返し、再試行時は成功させる」といったケース。
- 並行処理・タイミングの問題で InOrder が噛み合わない
- goroutine やチャネルを使ったコードでは、 厳密な呼び出し順の保証 が難しく、InOrder が期待通りに働かないことがあります。
- DoAndReturn で「呼ばれたタイミングごとに分岐する」ほうが自然な設計になる場合に有効です。
- 順序よりも「回数」や「引数」に注目したテストがしたい
- InOrder はあくまで「何番目に呼ばれるか」を重視します。一方で「ある引数を渡されたらこう返す」など、 呼び出しの条件 にフォーカスしたい場合は、DoAndReturn のほうが直感的な場合が多いです。
2-4. DoAndReturn の注意点
-
クロージャに状態を持たせると可読性が落ちる
- 先ほどの例のようにカウンタ変数をクロージャの外部に定義し、それをインクリメントしながら動きを分岐します。
- これが複雑化すると「次は 2 回目のはずが 3 回目になっている…」など、テストのバグを引き起こす可能性があります。
- テストコードとして読みやすいかどうか、 過度なロジックを盛り込まない よう注意しましょう。
-
呼び出しスコープが増えると管理が大変
- 例:テストコードのあちこちで
DoAndReturn
を駆使して状態を切り替えていると、挙動を追いづらくなります。 - あくまでも「InOrder では表現しづらい部分を補う」程度に留めるのがおすすめです。
- 例:テストコードのあちこちで
-
Times(...) と組み合わせることを忘れずに
- 「1回目だけこうする」「2回目までこうする」など、呼び出し回数を明示しておくと、 モック呼び出し回数が予期せず増えた場合にもテストが落ちて気づきやすい です。
-
AnyTimes()
やMaxTimes(n)
などの指定も活用して、「呼ばれても呼ばれなくてもいい呼び出し」と「必ず呼ばれる呼び出し」を区別しておくと、テストが理解しやすくなります。
2-5. InOrder と DoAndReturn のすみわけ
-
InOrder が向いている場合
- 「このメソッド→次にこのメソッド→さらにこのメソッド」といった、 順序 そのものが大事なロジックをテストしたいとき。
- 例:ネットワーク接続→認証→データ取得の順に絶対従う必要がある場合。
-
DoAndReturn が向いている場合
- 複数回呼ばれる 同じメソッド に対して呼び出しごとに返り値を切り替えたいとき。
- 順序よりも「呼び出し回数や引数をもとに動的に返す結果を変化させる」ほうが重視されるケース。
- 並行処理のタイミングが絡んでInOrder の厳密さが逆にネックになってしまうとき。
どちらの方法も モックテストの表現力を高めるテクニック なので、状況に応じて使い分けると良いでしょう。InOrder で素直に書ける場合は InOrder を使い、難しければ DoAndReturn を検討する、という段階的なアプローチがおすすめです。
3. サンプルコード:1回目はHello、2回目はWorld
ディレクトリ構成
$ tree ./
./
├── go.mod
├── go.sum
├── greeter.go
├── greeter_mock.go
└── greeter_test.go
0 directories, 5 files
-
greeter.go
テスト対象のインターフェイス Greeter と、実際に呼び出す関数 GreetingSequence を定義。 -
greeter_mock.go
モック生成コマンドで自動生成されるモックファイル。 -
greeter_test.go
テストコード本体。
テスト対象コード
// greeter.go
package greeter
// Greeter インターフェイス(モック生成対象)
type Greeter interface {
Greet() string
}
// GreetingSequence は Greeter を2回呼び出し、結果を結合して返す
func GreetingSequence(g Greeter) string {
first := g.Greet() // 1回目の呼び出し
second := g.Greet() // 2回目の呼び出し
return first + " " + second
}
-
ポイント:
Greeter
インターフェイスのGreet()
メソッドを2回呼び、返ってきた文字列をスペースで結合しているだけのシンプルな実装です。 - ここを「1回目はHello、2回目はWorld」と返すようにテストしたい、というのが今回の目標です。
モック生成コマンド
このプロジェクトでは、GoMock を用いてモックを生成する際に以下のコマンドを実行します
$ mockgen -destination=./greeter_mock.go -package=greeter . Greeter
-
-destination
で出力ファイル名(./greeter_mock.go
)を指定。 -
-package
でモックファイルのパッケージ名をgreeter
に指定。 -
.
は「現在のパッケージ(= greeter)を意味し」、その後にGreeter
を続けることで「現在のパッケージ内のGreeter
インターフェイスをモック生成する」という指定になります。 - 実行後、
greeter_mock.go
が生成され、NewMockGreeter
などのモックメソッドが利用できるようになります。
テストコード
InOrder を使ったテスト
// greeter_test.go
package greeter
import (
"testing"
"github.com/stretchr/testify/assert"
gomock "go.uber.org/mock/gomock"
)
func TestGreetingSequence_InOrder(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGreeter := NewMockGreeter(ctrl)
// InOrder を使って「1回目:Hello」「2回目:World」と順序を指定
gomock.InOrder(
mockGreeter.EXPECT().Greet().Return("Hello").Times(1),
mockGreeter.EXPECT().Greet().Return("World").Times(1),
)
got := GreetingSequence(mockGreeter)
want := "Hello World"
assert.Equal(t, want, got)
}
解説
-
gomock.InOrder(...)
1回目の呼び出し→ Hello、2回目の呼び出し→ World という順序を厳密に定義します。 -
Times(1)
それぞれ1回のみ呼ばれることを保証します。順番が逆転したり追加で呼ばれたりするとテストが失敗するため、「順序をしっかりテストしたい」ケースに最適です。
メリット
- テスト意図(「先にHello、次にWorld」)がコード上で明確になる。
異なる順序で呼ばれた場合は即座にテストが失敗し、誤った実装を早期に検知できる。
注意点
- 呼び出しが並列化されるなどで順番が保証しづらい場合、InOrder の厳密な制約が逆にネックになることがあります。
DoAndReturn を使ったテスト
func TestGreetingSequence(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGreeter := NewMockGreeter(ctrl)
// 呼び出し回数をカウントしながら返り値を変化させる
callCount := 0
mockGreeter.EXPECT().Greet().Times(2).DoAndReturn(func() string {
callCount++
if callCount == 1 {
return "Hello"
}
return "World"
})
got := GreetingSequence(mockGreeter)
want := "Hello World"
assert.Equal(t, want, got)
}
解説
- callCount を使う
DoAndReturn
に渡すクロージャの中で、呼び出し回数を自前でカウントし、1回目は "Hello"、2回目は "World" を返すようにしています。 -
Times(2)
必ず2回呼び出される前提でテストを行う設定です。 - 柔軟性が高い
3回目以降、引数の種類によって、など 複雑なロジック にも拡張しやすいのがメリットです。
メリット
- 同じメソッドを複数回呼んだときでも、返り値を自由に切り替えられる。
- 順序というより「回数」や「引数」に注目して挙動を変えたいときに便利。
注意点
- クロージャの外部変数を使いすぎると、テストが煩雑になりやすい。
- InOrder のように「順番が絶対に大事」というわけではなく、「呼び出し回数ごとに動作を切り替える」視点で書く点に留意する。
4. 結論
- InOrder は「順序を明確にテストしたい」ケースでの強力な味方。
- DoAndReturn は「回数や引数による返り値切り替え」を柔軟に書きたい場合に有用。
- いずれも GoMock の主要機能 であり、組み合わせて使うことで高度なテストが実現できる。
Discussion