Go中級 #6 テスティング — testing パッケージと table-driven
#5 context 深掘り の次、今回は — Go の自慢の一つ。標準ライブラリに含まれる強力なテスティングツール。
最初のテスト #
Go のテスティングは — 標準 testing パッケージと go test コマンドで動作します。追加ライブラリなしに。
package math
func Add(a, b int) int {
return a + b
}package math
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)
}
}ルール:
- ファイル名が
_test.goで終わる - 関数名が
Testで始まる、引数は*testing.T - 同じパッケージ内に置ける(または
package math_testで外部パッケージ)
実行 #
go test # 現在のパッケージ
go test ./... # すべてのパッケージ
go test -v # 詳細 (各テスト結果)
go test -run TestAdd # 特定のテストのみgo test -v がよく使われます — どのテストが実行されてどれが失敗したかが明確に見えます。
失敗を知らせる — t.Errorf vs t.Fatalf
#
func TestSomething(t *testing.T) {
if !condition1 {
t.Errorf("条件 1 失敗") // 失敗を記録 + 次の行も実行
}
if !condition2 {
t.Fatalf("条件 2 失敗") // 失敗を記録 + 即時終了
}
// ...
}Errorf / Error | Fatalf / Fatal | |
|---|---|---|
| 失敗の記録 | ✓ | ✓ |
| その後のコード実行 | ✓ | ✗ (即時終了) |
| 合う場面 | 独立した検証(複数を一つのテストに) | 以後のコードが意味のない状況 |
普通 Fatal は — セットアップが失敗したか nil ポインタなど、それ以上進んでも意味のない場面で使います。
Table-Driven テスト #
同じ関数に複数の入力を検査したいとき — Go の最も有名なテストパターンです。
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"正の数", 2, 3, 5},
{"負の数", -1, -2, -3},
{"混合", -5, 10, 5},
{"ゼロ", 0, 0, 0},
}
for _, tc := range tests {
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)
}
})
}
}肝心:
- ケースをスライスとして定義 — 新ケース追加が一行
t.Run(name, ...)でサブテスト — 各ケースが独立に実行され報告される- ケースごとに
nameがあり どれが失敗したか即座に分かる
go test -v の結果:
=== RUN TestAdd
=== RUN TestAdd/正の数
=== RUN TestAdd/負の数
=== RUN TestAdd/混合
=== RUN TestAdd/ゼロ
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/正の数 (0.00s)
--- PASS: TestAdd/負の数 (0.00s)
--- PASS: TestAdd/混合 (0.00s)
--- PASS: TestAdd/ゼロ (0.00s)特定のサブテストだけ実行 #
go test -run "TestAdd/正の数"これがほぼすべての Go コードの標準テストの形です。ケースをデータで表現し、テストロジックは一箇所に。
ヘルパー関数 — t.Helper()
#
テストでよく使う検査を関数として切り出すと、失敗メッセージにヘルパー関数の位置が表示されてどこで失敗したか見にくくなります。
func assertEqual(t *testing.T, got, want int) {
t.Helper() // この関数の呼び出し元の位置を表示
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestAdd(t *testing.T) {
assertEqual(t, Add(2, 3), 5)
}t.Helper() 一行を追加すれば — 失敗メッセージにヘルパー関数ではなく 呼び出し位置 が表示されます。ヘルパーをよく作るならほぼ必須。
セットアップとクリーンアップ — t.Cleanup
#
テストごとにセットアップ/クリーンアップが必要なら。
func TestWithDB(t *testing.T) {
db := setupDB(t)
t.Cleanup(func() {
db.Close()
})
// テスト本文
}t.Cleanup は テストが終わるとき自動実行 されます。成功/失敗に関係なく。defer と似ていますが — テストヘルパー関数でも動作します(ヘルパーが終わっても cleanup はテスト終了まで生きている)。
一時ディレクトリ — t.TempDir
#
func TestFileWrite(t *testing.T) {
dir := t.TempDir() // 自動的に整理される一時ディレクトリ
path := filepath.Join(dir, "test.txt")
os.WriteFile(path, []byte("hi"), 0644)
// テストが終わると dir 自動削除
}よく使われるヘルパー。直接 os.MkdirTemp と defer os.RemoveAll を書くより短く安全です。
外部パッケージテスト — _test サフィックス
#
同じフォルダのテストファイルは普通同じパッケージ名 — そうすると unexported な関数もテスト可能。ただし意図的に 外部から見る視点 でテストしたいとき:
// math/add_test.go
package math_test // _test サフィックス
import (
"testing"
"path/to/math" // 外部から import するように
)
func TestAdd(t *testing.T) {
got := math.Add(2, 3)
// ...
}そうすると — exported API だけでテストするように強制されます。大きなライブラリでは二種類を混ぜて使うこともあります。
ベンチマーク — BenchmarkXxx
#
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}ルール:
- 関数名が
Benchmarkで始まる、引数*testing.B for i := 0; i < b.N; i++の中に測定するコードb.Nは Go が自動的に調整 — 反復回数が意味ある測定に十分になるまで
go test -bench=.
go test -bench=. -benchmem # メモリ割り当ても測定BenchmarkAdd-8 1000000000 0.30 ns/opAdd 一回が平均 0.30 ナノ秒かかったという意味。CPU コア数(8)も一緒に表示。
b.ResetTimer() — セットアップコストを除外
#
func BenchmarkParse(b *testing.B) {
data := generateBigData() // セットアップ — 測定から除外
b.ResetTimer()
for i := 0; i < b.N; i++ {
Parse(data)
}
}セットアップ時間が測定に入ると正確な結果が出ないので — セットアップ後に ResetTimer()。
Example 関数 — ドキュメントでありテスト #
func ExampleAdd() {
fmt.Println(Add(2, 3))
// Output: 5
}Example で始まる関数 + 末尾の // Output: コメント。Go の godoc/pkg.go.dev にコード例として表示され、同時にテストとしても実行 されます。Output が違うと失敗。
ライブラリのドキュメントを豊富にする標準ツールです。
モッキング — Go のアプローチ #
Go にはモッキングライブラリ(testify、gomock)もありますが — インターフェースをうまく使うだけで標準ツールだけで モッキングができます。
type UserStore interface {
GetUser(id string) (*User, error)
}
type Service struct {
store UserStore
}
// 実際の実装
type DBStore struct{ /* ... */ }
func (s *DBStore) GetUser(id string) (*User, error) { /* DB 検索 */ }
// テスト用 fake
type fakeStore struct {
users map[string]*User
}
func (s *fakeStore) GetUser(id string) (*User, error) {
if u, ok := s.users[id]; ok {
return u, nil
}
return nil, ErrNotFound
}
// テスト
func TestService(t *testing.T) {
store := &fakeStore{
users: map[string]*User{"u1": {Name: "カーティス"}},
}
svc := &Service{store: store}
user, err := svc.LookupUser("u1")
// ...
}#1 インターフェース の暗黙的実装のおかげで — テスト用実装を自由に差し込めます。モッキングライブラリなしでも十分です。
httptest — HTTP テスト
#
標準ライブラリに HTTP ハンドラをテストするツールがあります。
import (
"net/http"
"net/http/httptest"
)
func TestHandler(t *testing.T) {
handler := http.HandlerFunc(myHandler)
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL + "/api/users")
// 検証
}httptest.NewRecorder も — 本物のサーバを立ち上げずにハンドラだけ呼んで結果を検査できます。実践 #6 テストとデプロイ で詳しく。
カバレッジ — -cover
#
go test -cover
go test -coverprofile=cover.out
go tool cover -html=cover.out # HTML で可視化テストがどの行を実行したか — 一つのコマンドで。意外と軽量なツールです。
よく使うテストライブラリ — testify #
標準だけで十分ですが — assertion が長くなれば testify がよく使われます。
import "github.com/stretchr/testify/assert"
func TestSomething(t *testing.T) {
got := Add(2, 3)
assert.Equal(t, 5, got)
assert.NoError(t, err)
assert.NotNil(t, result)
}if/Errorf より短くて意図が明確。ただし標準だけでも十分動作するので、チームのコンベンションに合わせれば良いです。
まとめ #
今回の記事で整理した内容:
- ファイル
_test.go、関数Test...、引数*testing.T t.Errorf(続行) vst.Fatalf(即時中断)- table-driven +
t.Runが Go の標準パターン t.Helper()でヘルパー関数で呼び出し元の位置を表示t.Cleanup、t.TempDirのような自動クリーンアップツール_testサフィックスパッケージで外部視点のテストBenchmark...+b.N+-bench=.Example...はドキュメントでありテスト- インターフェース + fake で標準モッキング
httptest、-coverのようなビルトインサポート
次の記事(#7 標準ライブラリツアー)では — Go の分厚い標準ライブラリの中でよく使われるもの(io、fmt、strings、time、encoding/json など)を一気に見渡します。