はじめに
Goのテストは標準機能が強い一方で、実務では次に詰まりやすいです。
- テストを書いたのに壊れやすい
- モックが増えて何を担保しているか分からない
- integrationテストが遅くて回らない
この記事は「理想論」ではなく、チームで回る最小ルールをテンプレとしてまとめます。
先に結論 最小のテスト方針
- unit は純粋関数と境界の分岐を厚くする
- integration はDBや外部I/Oとの契約を薄く広く取る
- 遅いテストは毎回回さないが、CIでは必ず回す
- テーブル駆動で増やしやすくする
テーブル駆動のテンプレ
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"ok", 1, 2, 3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.want {
t.Fatalf("got=%d want=%d", got, tt.want)
}
})
}
}
unit と integration の線引き
- unit
- 入力と出力が明確で、外部依存がない
- 例: バリデーション、変換、状態遷移
- integration
- DBやHTTPなど、外部との契約を確かめる
- 例: repository層、migration、HTTPハンドラ
線引きが曖昧だと、遅いテストが増えて運用が止まります。
失敗しにくいテストのコツ
- 時刻や乱数は注入する
- グローバル状態を持たない
- 並行処理を含むテストは待ち合わせを明示する
- エラー型は文字列ではなく errors.Is / As で検証する
CI運用テンプレ
- 速いテスト
- プルリクごとに必須
- integration
- mainブランチ、夜間、または変更があったときだけ必須
- カバレッジ
- 数字を目的化しない。壊れやすい境界を優先する
レビュー用チェックリスト
- テストが何を守っているか(契約)が読める
- 文字列比較ではなく errors.Is / As を使っている
- テーブル駆動でケース追加が容易
- 時刻や乱数が固定または注入されている
- integrationテストの起動条件が運用として決まっている