3-3. testing パッケージ - Go標準のテスト文化
所要時間: 40-50分 コミット内容:
~/learn/go/level3/day03/に各種テストパターンの実装
このレッスンのゴール
-
func TestXxx(t *testing.T)の基本形を書ける -
t.Error/t.Fatalを場面で使い分けられる -
t.Runでサブテストに名前を付け、-runで個別実行 -
t.Helper/t.Cleanup/t.TempDirでテストヘルパーを書ける -
go test -race/-cover/-benchを使い分けられる -
Example関数でドキュメント兼テストを書く
なぜ学ぶか
「TS だと Jest だった、Go の test はどう違う?」 - Go のテストは DSL なし、Go の文法そのもの。describe/expect も beforeEach もない。func TestXxx(t *testing.T) と t.Errorf("...") だけで成り立つ。最初は「これだけ?」と思うが、学習コストが極限まで低いことが Go コミュニティの設計思想。一方で t.Parallel() の落とし穴、t.Helper() の意味、race detector の活用法を知らないと、フレーキーテストや本番でしか出ない競合バグを量産する。
前章とのつながり
3-2_CRUDハンドラ で UserRepository インターフェースを書いた。本章ではそれを モック実装で単体テスト する基盤を作る。Repository インターフェースがあるからテストが書ける、という構造を体感する。
これができると何が嬉しいか
- テストを書くハードルが激減 - 設定ファイル不要、関数1つ書くだけ
-raceで並行アクセスのバグを CI で検出 - 本番障害の予防t.Helper()で読みやすいテストヘルパー が書ける- Example 関数で「動くドキュメント」 が書ける
ストーリー導入: テストは「設定ファイルなし、ただの関数」
Jest なら jest.config.js、Mocha なら .mocharc.json、Vitest なら vitest.config.ts… これら全てが Go には 不要。*_test.go ファイル、Test で始まる関数、go test ./... コマンド - これだけ。「DSL を作らない、Go の文法でテストを書く」哲学。testify を入れる前に標準だけで書ける力を付ける。
大前提: Go のテストはなぜ「素朴」か
Jest / Mocha / RSpec / pytest を触ってきた人は、Go のテストを最初に書いた時に「これだけ?」と思うはずです。describe も expect().toBe() も before/after each も無い。あるのは:
func TestXxx(t *testing.T)という関数t.Errorf("expected %v, got %v", want, got)というエラー報告go test ./...というコマンド
それだけ。これは Go コミュニティの 「DSL を作らない、Go の文法でテストを書く」 という哲学からきています。testify/assert を使う人もいるが、コア層は標準パッケージで十分という派が依然強い。
理由は3つ:
- 学習コストが低い: 文法が普通の Go なので、Go を書ければテストも書ける
- 読みやすい: 隠蔽が少なく、何をテストしているか一目で分かる
- デバッグ容易: スタックトレースが綺麗、DSL の中で迷子にならない
このレッスンでは 標準パッケージだけで書く力 を最優先で身につけ、testify などの位置付けを最後に触れます。
セッション①: testing パッケージの基本(25分)
0. 録画と作業ディレクトリ
mkdir -p ~/log ~/learn/go/level3/day03
cd ~/learn/go/level3/day03
script ~/log/go_level3_day03.log
go mod init example.com/testing-play1. 最小のテスト
// adder.go
package adder
func Add(a, b int) int {
return a + b
}// adder_test.go
package adder
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d, want %d", got, want)
}
}go test ./...Go の test の3つのお約束
- ファイル名は
*_test.goで終わる- 関数名は
Testで始まり、続く文字は大文字(TestAddOK,Testadd認識されない)- シグネチャは
func TestXxx(t *testing.T)これだけで
go testが自動で見つけて実行する。設定ファイル不要。
*_test.goは通常ビルドには含まれず、go buildの成果物に混ざらない。テストでだけ使うパッケージ(mock 等)を*_test.goだけが import する形にすると、本番バイナリに余分なコードが入らない。これも Go の設計思想の一つ。
2. t.Error vs t.Fatal の使い分け
func TestUserRepo(t *testing.T) {
repo := setupRepo(t)
u, err := repo.Create(ctx, &User{Email: "a@b.com"})
if err != nil {
t.Fatalf("Create failed: %v", err) // 致命的: 続行不能
}
if u.ID == 0 {
t.Errorf("ID = 0, want non-zero") // 続行可能
}
if u.Email != "a@b.com" {
t.Errorf("Email = %q, want a@b.com", u.Email)
}
}
t.Errorとt.Fatalの違い
メソッド 挙動 使い所 t.Error/t.Errorfエラー記録、テスト関数は続行 複数の独立したアサーションがある時 t.Fatal/t.Fatalfエラー記録、テスト関数を即終了 続けても無意味な場合(前提条件が崩れた、nil参照になる) 例:
err != nilでFatal、その後のフィールド検査はErrorという使い分けが定石。errが non-nil なのに次のu.IDを見たら nil pointer で panic する、というのを防ぐ。
goroutine の中で t.Fatal を呼べない
// NG go func() { if err != nil { t.Fatal(err) // panic: Fatal called from non-test goroutine } }()
t.Fatalは内部的にruntime.Goexitを呼ぶが、これは呼び出し元の goroutine しか終わらせない。テストの goroutine から呼ぶと未定義動作。解決:
t.Errorfで記録するか、chan errorでメインに送ってからメイン側でt.Fatal。
3. テーブル駆動の入り口(t.Run)
func TestAdd_Table(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"zero", 0, 0, 0},
{"negative", -1, -2, -3},
{"overflow_ish", 1, 1, 2},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := Add(tc.a, tc.b)
if got != tc.want {
t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
})
}
}# 全部実行
go test -v ./...
# 特定のサブテストだけ
go test -run TestAdd_Table/zero ./...
t.Runの威力
- ケース単位で名前が付く: 失敗時に「どのケースが落ちたか」が
TestAdd_Table/positiveのような形式で出る- 個別に実行できる:
-run TestAdd_Table/zeroでピンポイントテスト- 並列化の単位になる:
t.Parallel()をサブテスト内で呼べる- 共通の setup を1度だけ書ける: ループの外で DB セットアップ → ループの中で Run
詳細は
3-4_テーブル駆動テスト.mdで深掘りする。
4. テストヘルパー
func mustCreateUser(t *testing.T, repo UserRepository, email string) *User {
t.Helper() // ← これがポイント
u, err := repo.Create(ctx, &User{Email: email})
if err != nil {
t.Fatalf("create user: %v", err)
}
return u
}
func TestSomething(t *testing.T) {
u := mustCreateUser(t, repo, "x@y.com")
// ...
}
t.Helper()を呼ぶとエラー報告が呼び出し元の行番号になる
t.Helper()を呼ばないと、エラーが起きた時のスタックトレースが ヘルパー関数の行 を指す。デバッグで「あれ、このヘルパーで失敗した?」と混乱する。
t.Helper()を最初の行で呼ぶと、Go のテストランナーは「この関数はヘルパーだから、エラーは呼び出し元の行を表示してね」と判断する。自作ヘルパーには必ずt.Helper()を入れる。
5. setup / teardown とサブテスト
func TestUserRepo(t *testing.T) {
db := setupDB(t) // ヘルパー
t.Cleanup(func() {
db.Close() // テスト終了時に実行
})
repo := &UserSQLRepo{DB: db}
t.Run("Create", func(t *testing.T) {
// ...
})
t.Run("Get", func(t *testing.T) {
// ...
})
}
t.Cleanupは LIFO で実行されるGo 1.14 で導入された。テスト終了時に必ず実行される。defer の代わりに
t.Cleanupを使うと、t.Runのサブテスト内・ヘルパー関数内など、defer のスコープを跨いでもクリーンアップが効く。func setupTempFile(t *testing.T) string { t.Helper() f, _ := os.CreateTemp("", "test-*") t.Cleanup(func() { os.Remove(f.Name()) }) return f.Name() }ヘルパー側で
t.Cleanupを呼ぶと、テスト関数側は気にしなくていい。「使うとついでに片付いてくれる」 関数を作れるのが強力。
6. テストデータ: testdata/ と t.TempDir
// testdata ディレクトリの中はテスト時に使うフィクスチャ
// testdata/input.json
// testdata/expected.json
func TestParse(t *testing.T) {
data, err := os.ReadFile("testdata/input.json")
if err != nil {
t.Fatal(err)
}
// ...
}
func TestWritesFile(t *testing.T) {
dir := t.TempDir() // 自動で作成・テスト終了時に削除
path := filepath.Join(dir, "out.txt")
if err := writeOutput(path); err != nil {
t.Fatal(err)
}
// ファイル検証
}
testdata/は特別なディレクトリGo のツール(
go build,go vet)はtestdata/という名前のディレクトリを 無視する(コンパイル対象にしない)。だから「テスト用の壊れた JSON」や「サンプル CSV」を置いても本番ビルドに影響しない。慣習: フィクスチャは
testdata/に置く。t.TempDir は Go 1.15+。テストごとに 専用のテンポラリディレクトリ を作って、終了時に自動削除。
os.MkdirTemp+defer os.RemoveAllを書く必要が無い。ファイルを生成するテストの定番。
セッション②: カバレッジ・ベンチマーク・並列化(25-30分)
7. カバレッジ計測
# テキスト形式(合計だけ)
go test -cover ./...
# 詳細プロファイル
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out # 関数ごとの被覆率
go tool cover -html=coverage.out # ブラウザで色付き可視化カバレッジの読み方
- 行カバレッジ: その行が一度でも実行されたか。Go の
-coverはこれ- 分岐カバレッジ: if/else の各分岐が実行されたか。Go では公式未対応
HTML出力が一番役に立つ: 緑が「テストされてる」、赤が「されてない」、灰色が「コメントなど」。赤いコードを目視で減らす のがレビューでよくやる作業。
カバレッジ100%を盲信しない
カバレッジ100% でも、境界値・エラー経路・並行アクセス がテストされていないことは普通にある。
func Divide(a, b int) int { return a / b // b=0 で panic するが、テストは a=10/b=2 だけ → 100%表示 }カバレッジは「足りない場所を見つける道具」であって、「品質保証の指標」ではない。重要なエッジケースを意識的にテストに含める ことが本質。
8. ベンチマーク
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Add(2, 3)
}
}
func BenchmarkParseJSON(b *testing.B) {
payload := []byte(`{"name":"alice","age":30}`)
b.ResetTimer() // 準備の時間を計測から除外
for i := 0; i < b.N; i++ {
var u User
if err := json.Unmarshal(payload, &u); err != nil {
b.Fatal(err)
}
}
}go test -bench=. -benchmem ./...
# BenchmarkAdd-8 1000000000 0.3 ns/op 0 B/op 0 allocs/op
# BenchmarkParseJSON-8 5000000 250 ns/op 192 B/op 4 allocs/opベンチマークの読み方
BenchmarkAdd-8: 末尾の-8は GOMAXPROCS(並列数)1000000000: 実行回数(b.Nの最終値)0.3 ns/op: 1回あたりのナノ秒192 B/op: 1回あたりのアロケーション量4 allocs/op: 1回あたりのアロケーション回数
allocs/opが0でない箇所は最適化候補。Go の GC は速いが、ホットパスでアロケーションが発生するとレイテンシ尾が伸びる。
b.ResetTimer()を入れる癖ベンチマークの 準備フェーズ(テストデータ生成、DB接続)の時間を計測対象から外すために
b.ResetTimer()を入れる。これを忘れると、ベンチ結果に準備コストが混ざって正確な計測にならない。
9. t.Parallel と落とし穴
func TestThings(t *testing.T) {
t.Run("A", func(t *testing.T) {
t.Parallel()
// 並列実行される
})
t.Run("B", func(t *testing.T) {
t.Parallel()
})
}
t.Parallel()の効果と落とし穴サブテストを並列実行する。CPU マルチコアを活かせて速い。
典型的な落とし穴1: ループ変数キャプチャ(Go 1.22 未満)
// Go 1.22 未満で N G なパターン for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // tc が共有変数なので、最後のケースだけ N回実行される }) } // 対策 for _, tc := range cases { tc := tc // ローカル変数にコピー t.Run(tc.name, func(t *testing.T) { t.Parallel() // ... }) }Go 1.22 以降では
forループのスコープが変わって不要になったが、古いコードで踏むトラップ。典型的な落とし穴2: 共有状態
var counter int func TestX(t *testing.T) { t.Run("A", func(t *testing.T) { t.Parallel() counter++ // データ競合 }) t.Run("B", func(t *testing.T) { t.Parallel() counter++ }) }並列化したテストでグローバル変数を変更すると データ競合。
go test -raceでこれを検出できる。
race detector を CI で必ず回す
go test -race ./...Go の data race detector は実用的でない遅さではないので、CI では必ず
-raceを付ける。本番障害の多くは「ローカルでは気付かない並行アクセス」。race detector が見つけてくれる。
10. Example 関数(実行可能なドキュメント)
func ExampleAdd() {
sum := Add(2, 3)
fmt.Println(sum)
// Output: 5
}Example 関数は「ドキュメント + テスト」の二刀流
go docやpkg.go.devでパッケージのドキュメントに そのまま表示 されるgo testで実行されて、// Output:コメントと標準出力を比較するドキュメントが「コンパイル可能・テストされた状態」を保てる。標準ライブラリ(
fmt,strings)はこれを大量に使っている。// Output: // 厳密に一致 // Unordered output: // 順不同で一致(map イテレーションなど)
11. テストの整理: 同パッケージ vs 別パッケージ
// adder_test.go
package adder // 同じパッケージ。private 関数も呼べる
// adder_external_test.go
package adder_test // 別パッケージ。public API だけで書く
_testサフィックスを付けたパッケージ名Go では1つのディレクトリに2種類のテストパッケージを置ける:
- ホワイトボックステスト (
package adder): 内部実装にアクセス可能- ブラックボックステスト (
package adder_test): 外部から見える API だけで書くブラックボックステストは「外部ユーザー目線の API テスト」になる。両方を意識して使い分け ると、内部実装の変更で破壊されにくいテストが書ける。
12. テストの実行制御
# 詳細出力
go test -v ./...
# 短いテストだけ実行(testing.Short() で分岐)
go test -short ./...
# タイムアウト変更(デフォルト10分)
go test -timeout 30s ./...
# 失敗時に即座に止める
go test -failfast ./...
# 特定のテスト名で絞る(正規表現)
go test -run "TestUser/Create" ./...
# キャッシュ無効化
go test -count=1 ./...
testing.Short()で重いテストをスキップfunc TestSlowIntegration(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") } // 重い処理 }
go test -shortを渡すと、testing.Short()がtrueを返す。ローカル開発では-shortで早く回し、CI では普通に回す という運用が定石。
13. アンチパターン集
アンチパターン: 環境依存テスト
// NG func TestNow(t *testing.T) { n := time.Now() // n を使った検証 → 実行時刻で結果が変わる }解決: 「時刻を返す関数」を依存性注入する。
type Clock interface { Now() time.Time } type fakeClock struct{ t time.Time } func (c fakeClock) Now() time.Time { return c.t }
アンチパターン: ネットワーク依存
// NG resp, _ := http.Get("https://api.example.com/...")外部APIに依存すると CI で落ちる、遅い、叩きすぎてレート制限。 解決:
httptest.NewServerでモックサーバーを立てる。3-4_テーブル駆動テスト.mdで詳しくやる。
アンチパターン: テストの順序依存
// NG: TestA で作ったレコードを TestB が前提にしているGo のテストは 並列・順序未定義 が前提。各テストは「ゼロから自立する」よう書く。setup を
t.Runの外で1回やる場合も、サブテスト間の独立性は保つ。
アンチパターン: フレーキーテスト
「たまに落ちる」テスト。原因は大抵:
time.Sleep(100*time.Millisecond)で待つ → CI のマシンによって変動- グローバル状態の競合
- 乱数シードを固定していない
「待ち時間」を入れたくなったら、ポーリング + タイムアウト に書き換える。
deadline := time.Now().Add(2 * time.Second) for time.Now().Before(deadline) { if condition() { return // 成功 } time.Sleep(10 * time.Millisecond) } t.Fatal("condition not met within timeout")
14. testify は使うべき?
// testify を使う場合
import "github.com/stretchr/testify/assert"
func TestSomething(t *testing.T) {
u, err := repo.GetByID(ctx, 1)
assert.NoError(t, err)
assert.Equal(t, "alice", u.Name)
assert.NotEmpty(t, u.Email)
}testify の賛否
賛: アサーションが短く読める、エラーメッセージが綺麗、
assert.ElementsMatchなどのリッチな比較否: 「Go なのに Java/Ruby っぽい DSL になる」、エラー時のスタックが分かりにくくなる場合、追加依存
コミュニティの空気感:
- 標準パッケージ +
t.Errorf派: 「Go らしく素朴に」- testify 派: 「冗長な if 文を書きたくない」
- go-cmp 派(Google系):
cmp.Diffで構造体比較個人的おすすめ: 標準で書けるところは標準、構造体の deep equal だけ
go-cmpを使う、テスト中の長い if 文に疲れたらtestify/require(requireはFatal系の挙動)を入れる。
go-cmp で構造体比較
import "github.com/google/go-cmp/cmp" if diff := cmp.Diff(want, got); diff != "" { t.Errorf("mismatch (-want +got):\n%s", diff) }構造体の深い比較で差分を綺麗に出してくれる。
reflect.DeepEqualより読みやすい出力。
練習課題
3-2_CRUDハンドラ.md で作った UserRepository 実装に対して:
TestUserRepo_Createを書く(成功ケース・重複メールエラーケース)t.Runでケース名を付けるt.Cleanupでテスト後に DB を綺麗にするt.Helper()を使ったヘルパー関数を作るgo test -race ./...を通すgo test -coverでカバレッジを確認BenchmarkUserRepo_GetByIDを書いて1クエリの時間を計測
締め: git で証跡を残す
cd ~/learn/go/level3/day03
git add .
git commit -m "feat(go): testing パッケージで基礎テストパターンを実装"アンチパターン集 - やらかし事例
テスト設計のやらかし
1. goroutine 内で
t.Fatalgo func() { t.Fatal(err) }() // panic
t.Fatalは呼び出し元 goroutine しか終わらせない。t.Errorに変えるか chan で送る。2. ループ変数キャプチャ罠(Go 1.21 以前)
for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() // tc が共有変数 → 最後のケースだけ N回実行 }) }Go 1.22+ で修正済。古いコードでは
tc := tcでコピー。3. テストの順序依存 「TestA で作った行を TestB が前提」 → 並列実行で壊れる。各テストが自立すべし。
4.
time.Sleepで待つフレーキーテスト CI のマシン速度差で落ちる。ポーリング+タイムアウトに置き換え。5. ネットワーク依存テスト
http.Get("https://api.example.com/...") // CI で落ちる
httptest.NewServerでモック化。6.
t.Helper()忘れ エラーがヘルパー関数の行を指す → デバッグで混乱。自作ヘルパー冒頭でt.Helper()必須。
対比表で違いを明確化
t.Errorvst.Fatal
メソッド 挙動 用途 t.Error/t.Errorf記録のみ、続行 複数アサーション、独立 t.Fatal/t.Fatalf記録 + 即終了 前提崩れ、続けると nil panic
標準 vs testify vs go-cmp
観点 標準 testify go-cmp 学習コスト 低 中 中 構造体比較 reflect.DeepEqualassert.Equalcmp.Diffエラーメッセージ 自前で書く 自動整形 差分表示が綺麗 推奨 全ての基礎 短く書きたい時 構造体 deep equal
ホワイトボックス vs ブラックボックス
観点 package foopackage foo_testアクセス private OK public のみ 用途 内部実装の単体テスト 外部 API テスト 場所 foo/foo_test.go foo/foo_external_test.go
自己評価チェックリスト
手を動かせた
-
func TestXxx(t *testing.T)を書いてgo testで実行 -
t.Runでサブテスト -
t.Helper()を自作ヘルパーに入れた -
t.Cleanupで後片付けを集約 -
t.TempDir()で一時ディレクトリ -
go test -coverでカバレッジ確認 -
BenchmarkXxx+-benchmemで計測 -
t.Parallel()+go test -raceで並行検証 -
ExampleXxxを1つ書いた
説明できる
-
t.Errorとt.Fatalの使い分け -
t.Helper()を呼ぶ意義 - race detector が見つけるバグの種類
-
testifyvs 標準パッケージの哲学
やらかし回避
- goroutine 内で
t.Fatalしない - テストの順序依存を作らない
-
time.Sleepでなくポーリングで待つ - ネットワーク依存は
httptestでモック化
詰まった時のチートシート
| やりたいこと | 書き方 |
|---|---|
| テスト関数 | func TestXxx(t *testing.T) |
| エラー記録(続行可) | t.Errorf("...") |
| エラー記録(即終了) | t.Fatalf("...") |
| サブテスト | t.Run("name", func(t *testing.T) { ... }) |
| クリーンアップ | t.Cleanup(func() { ... }) |
| ヘルパー | 関数冒頭で t.Helper() |
| 並列実行 | t.Parallel() |
| ベンチマーク | func BenchmarkXxx(b *testing.B) + for i := 0; i < b.N; i++ |
| 一時ディレクトリ | t.TempDir() |
| Example | func ExampleXxx() + // Output: コメント |
| 実行 | go test ./... |
| カバレッジ | go test -coverprofile=c.out -o /dev/null ./... |
| race detector | go test -race ./... |
「実務OK」基準
-coverと-raceを本能的に併用する: 単体・並行両方検証t.Helperを自作ヘルパーで使える: 行番号がズレない- フレーキーテストを書かない:
time.Sleepではなくポーリング、共有状態を排除 - Example で公開関数のドキュメントを書ける: ドキュメントとテストを一体化
- testify vs 標準パッケージの哲学を語れる
さらに深掘りするなら
- 公式: pkg.go.dev/testing
- 公式チュートリアル: Add a test (go.dev)
- The Go Blog: Profile-guided optimization - ベンチマークと最適化
- Advanced Testing with Go - Mitchell Hashimoto - HashiCorp 流テスト技法
- 書籍: 『Learning Go』(Jon Bodner) 第13章
- OSS:
golang/go標準ライブラリのテストコード自体が教科書
次のレッスン
3-4 テーブル駆動テスト でテーブル駆動テスト、モック、httptest、go-cmp の深掘りをする。
つながりの予告
- 本章の
t.Runを テーブル駆動の核として次章で本格活用 httptest.NewServerで外部 API モック化- mock 実装で 3-2_CRUDハンドラ の Repository をテスト