- Go言語
Go テストの書き方|テーブル駆動・httptestのコード例
Goのテストとは、xxx_test.go というファイルに func TestXxx(t *testing.T) を書き、go test で実行する標準の仕組みで、外部ライブラリなしでテーブル駆動テスト・HTTPのテスト・ベンチマーク・ファズテストまで書けます。 標準パッケージの testing と net/http/httptest だけで、ほとんどのテストがまかなえます。
この記事では、在庫(備品)を扱う架空の小さなパッケージを題材に、Go 1.27.1 で実際に go test -v・-cover・-run・-bench・-fuzz を実行した出力をすべて載せています。テーブル駆動テストと t.Parallel(Go 1.22 以降はループ変数のコピーが不要)、httptest.NewRecorder と httptest.NewServer の使い分け、Go 1.24 以降の b.Loop と t.Context、カバレッジHTMLの作り方、testify などの外部ライブラリを使うかの判断までを1本にまとめました。
Goのテストの早見表
ファイル・関数の決まりごと
| 種類 | 関数の形 | 実行コマンド | 用途 |
|---|---|---|---|
| テストファイル | xxx_test.go |
go test |
通常のビルドには含まれない |
| テスト | func TestXxx(t *testing.T) |
go test -v |
正しさの確認 |
| サブテスト | t.Run("名前", func(t *testing.T){...}) |
go test -run 'TestXxx/名前' |
ケースごとに名前を付ける |
| ベンチマーク | func BenchmarkXxx(b *testing.B) |
go test -bench . |
速度・メモリ割り当ての計測 |
| Example | func ExampleXxx() + // Output: |
go test |
ドキュメント兼テスト |
| ファズテスト | func FuzzXxx(f *testing.F) |
go test -fuzz FuzzXxx |
ランダム入力で性質を確認 |
| テスト用データ | testdata/ ディレクトリ |
― | go ツールがパッケージとして扱わない |
testing.T のよく使うメソッド
| メソッド | 意味 | 使いどころ |
|---|---|---|
t.Error / t.Errorf |
失敗を記録して続行 | 複数の値を比べたいとき |
t.Fatal / t.Fatalf |
失敗を記録してそのテストを終了 | 前提が崩れて先に進めないとき(エラーが nil でない等) |
t.Log / t.Logf |
ログ出力(-v か失敗時に表示) |
途中経過の確認 |
t.Run |
サブテストを作る | テーブル駆動テスト |
t.Parallel |
並列実行の対象にする | 独立したテストを速くする |
t.Helper |
失敗行を呼び出し元の行にする | 自作の assert 関数 |
t.Cleanup |
テスト終了時の後片付けを登録 | サーバー停止、ファイル削除 |
t.TempDir |
自動で消える一時ディレクトリ | ファイル入出力のテスト |
t.Setenv |
環境変数を設定し、終了時に戻す | 設定値のテスト(t.Parallel とは併用不可) |
t.Context |
テスト終了時にキャンセルされる Context(Go 1.24〜) | context.Background() の代わり |
t.Chdir |
カレントディレクトリを変更し、終了時に戻す(Go 1.24〜) | 相対パスを使う処理 |
t.Skip |
テストをスキップ | 環境が整っていないとき |
t.ArtifactDir |
出力ファイル用ディレクトリ(Go 1.26〜) | -artifacts と組み合わせて成果物を残す |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Go テストの書き方|テーブル駆動・httptestのコード例」
動作確認した環境
| 項目 | 内容 |
|---|---|
| Go | go1.27.1 linux/amd64 |
| 外部モジュール | なし(標準ライブラリのみ) |
| モジュール名 | example.com/stock(架空) |
以下の実行結果は、すべてこの環境で実際に出力したものです。
ファイル構成は次のとおりです。
stock/
├── go.mod
├── stock.go テスト対象のコード
├── stock_test.go テスト本体(テーブル駆動・httptest・testdata など)
├── example_test.go Example テスト
├── bench_fuzz_test.go ベンチマークとファズテスト
└── testdata/
└── item_c1.golden.json
テスト対象のコード
まずテストされる側のコードです。数量の文字列を数値にする関数、商品コードを整える関数、GET /items/{id} を返す HTTP ハンドラ、その API を呼ぶクライアントを用意しました。
// go.mod
module example.com/stock
go 1.27
// stock.go
// Package stock は在庫(備品)を扱う小さなサンプルです。
package stock
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
)
// ErrInvalidQuantity は数量が不正なときのエラーです。
var ErrInvalidQuantity = errors.New("invalid quantity")
// ParseQuantity は "12" や " 3個 " のような文字列を数量に変換します。
func ParseQuantity(s string) (int, error) {
s = strings.TrimSpace(s)
s = strings.TrimSuffix(s, "個")
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("%w: %q", ErrInvalidQuantity, s)
}
if n < 0 {
return 0, fmt.Errorf("%w: negative %d", ErrInvalidQuantity, n)
}
return n, nil
}
// NormalizeSKU は商品コードを大文字・ハイフン区切りに整えます。
func NormalizeSKU(s string) string {
s = strings.ToUpper(strings.TrimSpace(s))
s = strings.ReplaceAll(s, "_", "-")
return s
}
// Item は備品1件です。
type Item struct {
ID string `json:"id"`
Name string `json:"name"`
Stock int `json:"stock"`
}
// Store は在庫データの置き場所です。
type Store struct{ items map[string]Item }
// NewStore は Store を作ります。
func NewStore(items ...Item) *Store {
m := map[string]Item{}
for _, it := range items {
m[it.ID] = it
}
return &Store{items: m}
}
// Handler は GET /items/{id} を処理する http.Handler を返します。
func (s *Store) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
it, ok := s.items[r.PathValue("id")]
if !ok {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(it)
})
return mux
}
// Client は在庫 API のクライアントです。
type Client struct {
BaseURL string
HTTPClient *http.Client
}
// GetItem は baseURL/items/{id} を呼び出して Item を返します。
func (c *Client) GetItem(ctx context.Context, id string) (Item, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/items/"+id, nil)
if err != nil {
return Item{}, err
}
res, err := c.HTTPClient.Do(req)
if err != nil {
return Item{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Item{}, fmt.Errorf("unexpected status: %d", res.StatusCode)
}
var it Item
if err := json.NewDecoder(res.Body).Decode(&it); err != nil {
return Item{}, err
}
return it, nil
}
テスト本体(コピペで動く完成形)
stock_test.go です。いちばん小さいテスト、テーブル駆動テスト、t.Helper、httptest の2種類、testdata、TempDir と Setenv を1ファイルにまとめています。
// stock_test.go
package stock
import (
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// 1. いちばん小さいテスト
func TestNormalizeSKU(t *testing.T) {
got := NormalizeSKU(" pen_01 ")
want := "PEN-01"
if got != want {
t.Errorf("NormalizeSKU() = %q, want %q", got, want)
}
}
// 2. テーブル駆動テスト + t.Run + t.Parallel
func TestParseQuantity(t *testing.T) {
t.Parallel()
tests := []struct {
name string
in string
want int
wantErr error
}{
{name: "数字のみ", in: "12", want: 12},
{name: "前後の空白", in: " 7 ", want: 7},
{name: "個つき", in: "3個", want: 3},
{name: "ゼロ", in: "0", want: 0},
{name: "負の数", in: "-1", wantErr: ErrInvalidQuantity},
{name: "文字", in: "abc", wantErr: ErrInvalidQuantity},
{name: "空文字", in: "", wantErr: ErrInvalidQuantity},
}
for _, tt := range tests { // Go 1.22 以降は tt := tt が不要
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ParseQuantity(tt.in)
if !errors.Is(err, tt.wantErr) {
t.Fatalf("ParseQuantity(%q) error = %v, want %v", tt.in, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("ParseQuantity(%q) = %d, want %d", tt.in, got, tt.want)
}
})
}
}
// 3. t.Helper:失敗行を呼び出し元にする
func assertStatus(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("status = %d, want %d", got, want)
}
}
// 4. httptest.NewRecorder:ハンドラ単体テスト
func TestHandler(t *testing.T) {
store := NewStore(Item{ID: "p1", Name: "ペン", Stock: 30})
h := store.Handler()
tests := []struct {
name string
path string
wantStatus int
wantBody string
}{
{"存在するID", "/items/p1", http.StatusOK, `"name":"ペン"`},
{"存在しないID", "/items/zz", http.StatusNotFound, "not found"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
assertStatus(t, rec.Code, tt.wantStatus)
if !strings.Contains(rec.Body.String(), tt.wantBody) {
t.Errorf("body = %q, want contains %q", rec.Body.String(), tt.wantBody)
}
})
}
}
// 5. httptest.NewServer:HTTP クライアント側のテスト
func TestClientGetItem(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/items/n1" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"id":"n1","name":"ノート","stock":5}`))
}))
t.Cleanup(srv.Close) // テスト終了時にサーバーを止める
c := &Client{BaseURL: srv.URL, HTTPClient: srv.Client()}
got, err := c.GetItem(t.Context(), "n1") // Go 1.24 以降:テスト終了でキャンセルされる Context
if err != nil {
t.Fatalf("GetItem() error = %v", err)
}
want := Item{ID: "n1", Name: "ノート", Stock: 5}
if got != want {
t.Errorf("GetItem() = %+v, want %+v", got, want)
}
if _, err := c.GetItem(t.Context(), "xx"); err == nil {
t.Error("GetItem(xx) error = nil, want error")
}
}
// 6. testdata のゴールデンファイルと比較
func TestItemJSONGolden(t *testing.T) {
it := Item{ID: "c1", Name: "クリップ", Stock: 120}
got, err := json.MarshalIndent(it, "", " ")
if err != nil {
t.Fatal(err)
}
want, err := os.ReadFile(filepath.Join("testdata", "item_c1.golden.json"))
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(string(got)) != strings.TrimSpace(string(want)) {
t.Errorf("JSON mismatch\n got: %s\nwant: %s", got, want)
}
}
// 7. TempDir と Setenv
func TestTempDirAndSetenv(t *testing.T) {
dir := t.TempDir() // テスト終了時に自動で削除される
path := filepath.Join(dir, "stock.txt")
if err := os.WriteFile(path, []byte("12個"), 0o644); err != nil {
t.Fatal(err)
}
b, _ := os.ReadFile(path)
n, err := ParseQuantity(string(b))
if err != nil || n != 12 {
t.Fatalf("got %d, %v", n, err)
}
t.Setenv("STOCK_MODE", "test") // テスト終了時に元の値へ戻る
if os.Getenv("STOCK_MODE") != "test" {
t.Error("Setenv が反映されていません")
}
t.Logf("TempDir=%s", filepath.Base(dir))
}
testdata/item_c1.golden.json の中身です。
{
"id": "c1",
"name": "クリップ",
"stock": 120
}
go test -v の実行結果
go test -v ./...
実行結果(go1.27.1。後述の Example・ファズのシードも含む):
=== RUN TestNormalizeSKU
--- PASS: TestNormalizeSKU (0.00s)
=== RUN TestParseQuantity
=== PAUSE TestParseQuantity
=== RUN TestHandler
=== RUN TestHandler/存在するID
=== RUN TestHandler/存在しないID
--- PASS: TestHandler (0.00s)
--- PASS: TestHandler/存在するID (0.00s)
--- PASS: TestHandler/存在しないID (0.00s)
=== RUN TestClientGetItem
--- PASS: TestClientGetItem (0.00s)
=== RUN TestItemJSONGolden
--- PASS: TestItemJSONGolden (0.00s)
=== RUN TestTempDirAndSetenv
stock_test.go:149: TempDir=001
--- PASS: TestTempDirAndSetenv (0.00s)
=== CONT TestParseQuantity
=== RUN TestParseQuantity/数字のみ
=== PAUSE TestParseQuantity/数字のみ
=== RUN TestParseQuantity/前後の空白
=== PAUSE TestParseQuantity/前後の空白
=== RUN TestParseQuantity/個つき
=== PAUSE TestParseQuantity/個つき
=== RUN TestParseQuantity/ゼロ
=== PAUSE TestParseQuantity/ゼロ
=== RUN TestParseQuantity/負の数
=== PAUSE TestParseQuantity/負の数
=== RUN TestParseQuantity/文字
=== PAUSE TestParseQuantity/文字
=== RUN TestParseQuantity/空文字
=== PAUSE TestParseQuantity/空文字
=== CONT TestParseQuantity/数字のみ
=== CONT TestParseQuantity/空文字
=== CONT TestParseQuantity/文字
=== CONT TestParseQuantity/負の数
=== CONT TestParseQuantity/ゼロ
=== CONT TestParseQuantity/個つき
=== CONT TestParseQuantity/前後の空白
--- PASS: TestParseQuantity (0.00s)
--- PASS: TestParseQuantity/数字のみ (0.00s)
--- PASS: TestParseQuantity/空文字 (0.00s)
--- PASS: TestParseQuantity/文字 (0.00s)
--- PASS: TestParseQuantity/負の数 (0.00s)
--- PASS: TestParseQuantity/ゼロ (0.00s)
--- PASS: TestParseQuantity/個つき (0.00s)
--- PASS: TestParseQuantity/前後の空白 (0.00s)
=== RUN FuzzNormalizeSKU
=== RUN FuzzNormalizeSKU/seed#0
=== RUN FuzzNormalizeSKU/seed#1
--- PASS: FuzzNormalizeSKU (0.00s)
--- PASS: FuzzNormalizeSKU/seed#0 (0.00s)
--- PASS: FuzzNormalizeSKU/seed#1 (0.00s)
=== RUN ExampleNormalizeSKU
--- PASS: ExampleNormalizeSKU (0.00s)
=== RUN ExampleParseQuantity
--- PASS: ExampleParseQuantity (0.00s)
PASS
ok example.com/stock 0.008s
出力の読み方:
=== PAUSE→=== CONT:t.Parallel()を呼んだテストはいったん止まり、並列でないテストがすべて終わってから再開(CONT)します。- サブテストの順番がバラバラ:並列実行なので、完了した順に表示されます。
stock_test.go:149: TempDir=001:t.Logfの出力は-vを付けたときか、失敗したときに表示されます。- ファズテストもシード入力(
f.Add)だけは通常のgo testで毎回実行されます。
テーブル駆動テストの書き方
テーブル駆動テストとは、入力と期待値の組を構造体のスライス(表)に並べ、ループで同じ検証を回す書き方です。 Go の標準ライブラリでも広く使われています。
書き方のポイント
| ポイント | 理由 |
|---|---|
name フィールドを持たせ t.Run(tt.name, ...) で回す |
失敗したケースが名前でわかり、-run で1件だけ実行できる |
期待するエラーは wantErr error にして errors.Is で比べる |
エラーの種類まで確認できる |
前提が崩れたら t.Fatalf、値の比較は t.Errorf |
Fatal はそのサブテストだけを終了し、他のケースは続く |
メッセージは 関数(入力) = 実際, want 期待 の形 |
Go の慣習で、ログを見ただけで原因がわかる |
失敗したときの出力
わざと期待値を間違えたケースを入れて実行すると、次のように表示されます(実際の出力)。
{"正しい期待値", "5", 5},
{"わざと間違えた期待値", "8個", 80},
--- FAIL: TestFailDemo (0.00s)
--- FAIL: TestFailDemo/わざと間違えた期待値 (0.00s)
fail_test.go:18: ParseQuantity("8個") = 8, want 80
--- FAIL: TestHelperDemo (0.00s)
fail_test.go:25: status = 404, want 200
FAIL
FAIL example.com/stock 0.004s
FAIL
失敗したサブテストの名前・入力・実際の値・期待値が1行でわかります。 下の TestHelperDemo は assertStatus(t, 404, 200) を呼んだだけのテストですが、t.Helper() のおかげで、assert 関数の中ではなく呼び出し元(fail_test.go:25)の行が表示されています。
t.Parallel とループ変数(Go 1.22 以降)
for _, tt := range tests {
tt := tt // Go 1.21 までは必要だった
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// ...
})
}
Go 1.22 でループ変数がイテレーションごとに新しく作られるようになったため、tt := tt は不要になりました。 この挙動は go.mod の go ディレクティブが 1.22 以上のモジュールで有効です。本記事のコードは go 1.27 なので、コピーなしで並列実行しても各ケースが正しい値を使っていることを上の実行結果で確認できます。
t.Parallel を付けるかの判断
| 状況 | t.Parallel | 理由 |
|---|---|---|
| 純粋な関数(入力→出力のみ) | 付ける | 互いに影響しないので速くなる |
t.Setenv や t.Chdir を使う |
付けない(併用するとパニック) | プロセス全体の状態を変えるため |
| 共有の変数・ファイルを書き換える | 付けない/分離してから付ける | 結果が実行順に左右される |
| 1件あたりが一瞬で終わる小さなテスト | どちらでもよい | 効果は小さいが害もない |
httptest:NewRecorder と NewServer の使い分け
| 項目 | httptest.NewRecorder |
httptest.NewServer |
|---|---|---|
| テストする対象 | サーバー側のハンドラ | HTTP クライアント側のコード |
| ネットワーク | 使わない(関数呼び出し) | ローカルの実ポートで待ち受け |
| リクエストの作り方 | httptest.NewRequest(...) |
本物の http.Client で送る |
| 結果の取り方 | rec.Code・rec.Body・rec.Header() |
クライアントの戻り値 |
| 後片付け | 不要 | srv.Close()(t.Cleanup で登録) |
| 速さ | とても速い | 速い(実通信のぶん少し重い) |
| TLS | ― | httptest.NewTLSServer と srv.Client() で可 |
NewRecorder:ハンドラ単体のテスト
req := httptest.NewRequest(http.MethodGet, "/items/p1", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
// rec.Code == 200, rec.Body に JSON
ServeHTTP を直接呼ぶので、サーバーを立てずにステータスコード・本文・ヘッダーを確認できます。 Go 1.22 のパターン付きルーティング("GET /items/{id}" と r.PathValue("id"))も、ServeMux 経由で呼べばそのまま動きます(上の TestHandler で確認)。
NewServer:クライアント側のテスト
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"id":"n1","name":"ノート","stock":5}`))
}))
t.Cleanup(srv.Close)
c := &Client{BaseURL: srv.URL, HTTPClient: srv.Client()}
外部 API を呼ぶコードは、本物の API の代わりに httptest.NewServer で偽のサーバーを立て、その URL(srv.URL)をクライアントに渡します。 ベース URL と *http.Client を外から差し替えられる設計にしておくと、テストが書きやすくなります。
Example テスト
// example_test.go
package stock_test
import (
"fmt"
"example.com/stock"
)
func ExampleNormalizeSKU() {
fmt.Println(stock.NormalizeSKU(" pen_01 "))
// Output: PEN-01
}
func ExampleParseQuantity() {
n, err := stock.ParseQuantity("3個")
fmt.Println(n, err)
_, err = stock.ParseQuantity("abc")
fmt.Println(err)
// Output:
// 3 <nil>
// invalid quantity: "abc"
}
// Output: コメントと標準出力が一致すれば PASS になり、同じコードが go doc やpkg.go.dev の使用例として表示されます。 パッケージ名を stock_test にすると、外部の利用者と同じく公開 API だけを使ったテストになります。順序が決まらない出力(map のループなど)には // Unordered output: を使います。
ベンチマーク(b.Loop)
// bench_fuzz_test.go(前半)
package stock
import (
"strings"
"testing"
)
// ベンチマーク(Go 1.24 以降の b.Loop)
func BenchmarkParseQuantity(b *testing.B) {
for b.Loop() {
ParseQuantity(" 123個 ")
}
}
func BenchmarkNormalizeSKU(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
NormalizeSKU(" pen_01 ")
}
}
go test -run '^$' -bench . -benchmem
実行結果(go1.27.1。数値は環境によって変わります):
goos: linux
goarch: amd64
pkg: example.com/stock
cpu: Intel(R) Xeon(R) Processor @ 2.10GHz
BenchmarkParseQuantity-2 34258078 37.72 ns/op 0 B/op 0 allocs/op
BenchmarkNormalizeSKU-2 10145930 130.6 ns/op 16 B/op 2 allocs/op
PASS
ok example.com/stock 2.634s
| 列 | 意味 |
|---|---|
-2 |
使った CPU 数(GOMAXPROCS) |
34258078 |
実行回数 |
37.72 ns/op |
1回あたりの時間 |
0 B/op / 0 allocs/op |
1回あたりのメモリ割り当て量と回数(-benchmem か b.ReportAllocs()) |
for b.Loop() は Go 1.24 で追加された書き方で、従来の for i := 0; i < b.N; i++ と違い、準備処理の時間が自動で計測から外れ、ループ内の結果が最適化で消されにくくなっています。 Go 1.26 では、ループ本体のインライン化を妨げないよう改善されました。-run '^$' は「通常のテストを1つも実行しない」ための指定です。
ファズテスト
// bench_fuzz_test.go(後半)
// ファズテスト:どんな入力でも守られる性質を確かめる
func FuzzNormalizeSKU(f *testing.F) {
f.Add("pen_01")
f.Add(" note-a5 ")
f.Fuzz(func(t *testing.T, s string) {
got := NormalizeSKU(s)
if strings.Contains(got, "_") {
t.Errorf("NormalizeSKU(%q) = %q に _ が残っています", s, got)
}
if NormalizeSKU(got) != got {
t.Errorf("2回目で結果が変わりました: %q -> %q", got, NormalizeSKU(got))
}
})
}
go test -run '^$' -fuzz FuzzNormalizeSKU -fuzztime 10s -parallel 2 .
実行結果(go1.27.1):
fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed
fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 2 workers
fuzz: elapsed: 3s, execs: 16796 (5598/sec), new interesting: 44 (total: 46)
fuzz: elapsed: 6s, execs: 61481 (14895/sec), new interesting: 71 (total: 73)
fuzz: elapsed: 9s, execs: 83127 (7199/sec), new interesting: 77 (total: 79)
fuzz: elapsed: 11s, execs: 83127 (0/sec), new interesting: 77 (total: 79)
PASS
ok example.com/stock 11.019s
ファズテストは「期待値」ではなく「どんな入力でも成り立つ性質」を書くのがコツです。 ここでは「_ が残らない」「2回整えても結果が変わらない(冪等)」を確認しました。約8万回の入力で性質が保たれています。失敗する入力が見つかると testdata/fuzz/FuzzNormalizeSKU/ に保存され、以後は通常の go test で毎回その入力が再実行されます。-fuzz は1回に1つのパッケージ・1つのファズ関数だけ指定できます。
testdata ディレクトリの使い方
| 使い方 | 例 |
|---|---|
| ゴールデンファイル(期待する出力)を置く | testdata/item_c1.golden.json |
| 入力ファイルを置く | testdata/input.csv |
| ファズで見つかった入力が自動保存される | testdata/fuzz/FuzzXxx/ |
testdata という名前のディレクトリは go ツールがパッケージとして扱わないため、テスト専用のファイルを自由に置けます。 go test はパッケージのディレクトリをカレントディレクトリにして実行するので、filepath.Join("testdata", "...") の相対パスで読めます。書き込みが必要なファイルは testdata ではなく t.TempDir() に作ると、終了時に自動で消えます。
go test のオプション一覧
| オプション | 意味 | 例 |
|---|---|---|
-v |
テストごとの結果とログを表示 | go test -v ./... |
-run 正規表現 |
名前が一致するテストだけ実行(/ でサブテスト) |
go test -run 'TestHandler/存在しない' |
-skip 正規表現 |
一致するテストを除外 | go test -skip TestClient |
-count n |
n 回実行(-count=1 でキャッシュを使わない) |
go test -count=1 ./... |
-cover |
カバレッジ率を表示 | go test -cover ./... |
-coverprofile=file |
カバレッジの詳細をファイルに出力 | go test -coverprofile=cover.out |
-bench 正規表現 |
ベンチマークを実行 | go test -bench . -run '^$' |
-benchmem |
メモリ割り当ても表示 | go test -bench . -benchmem |
-benchtime |
ベンチマークの時間や回数 | -benchtime=3s、-benchtime=1000x |
-fuzz 正規表現 |
ファズテストを実行 | go test -fuzz FuzzNormalizeSKU |
-fuzztime |
ファズの実行時間 | -fuzztime 30s |
-race |
データ競合を検出 | go test -race ./... |
-parallel n |
並列実行するテストの最大数 | -parallel 4 |
-timeout |
全体の制限時間(既定 10 分) | -timeout 30s |
-short |
testing.Short() が true になる |
重いテストを飛ばす |
-failfast |
最初の失敗で止める | go test -failfast |
-shuffle on |
テストの実行順をランダムにする | 順序依存を見つける |
-json |
結果を JSON で出力 | CI のレポート用 |
-artifacts |
t.ArtifactDir() の出力を残す(Go 1.26〜) |
-outputdir と併用 |
-run で1件だけ実行する
go test -v -run 'TestHandler/存在しない' .
=== RUN TestHandler
=== RUN TestHandler/存在しないID
--- PASS: TestHandler (0.00s)
--- PASS: TestHandler/存在しないID (0.00s)
PASS
ok example.com/stock 0.006s
-run は正規表現で、/ の前がトップレベルのテスト名、後ろがサブテスト名に対応します。 日本語のサブテスト名でも絞り込めます。
カバレッジの確認と HTML レポートの作り方
go test -cover .
ok example.com/stock 0.011s coverage: 91.9% of statements
関数ごとの内訳と HTML レポートは次の3コマンドで作れます。
go test -coverprofile=cover.out .
go tool cover -func=cover.out
go tool cover -html=cover.out -o cover.html
実行結果(go1.27.1):
ok example.com/stock 0.007s coverage: 91.9% of statements
example.com/stock/stock.go:18: ParseQuantity 100.0%
example.com/stock/stock.go:32: NormalizeSKU 100.0%
example.com/stock/stock.go:49: NewStore 100.0%
example.com/stock/stock.go:58: Handler 100.0%
example.com/stock/stock.go:79: GetItem 76.9%
total: (statements) 91.9%
cover.html をブラウザで開くと、実行された行が緑、実行されなかった行が赤で表示されます。 -o を付けずに go tool cover -html=cover.out とすると、ブラウザが直接開きます。今回は GetItem が 76.9% で、リクエスト作成の失敗や JSON の解析エラーの分岐が未通過だとわかります。カバレッジは「テストが通っていない場所を探す道具」として使い、数値そのものを目標にしすぎないのがおすすめです。
testify などの外部ライブラリを使うか
この検証環境では外部モジュールを取得できないため、以下の testify のコードは構文の紹介のみで、未実行です。
// 未実行:github.com/stretchr/testify を使う場合の書き方
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseQuantityWithTestify(t *testing.T) {
got, err := ParseQuantity("12")
require.NoError(t, err) // 失敗したらここで終了(t.Fatal 相当)
assert.Equal(t, 12, got) // 失敗しても続行(t.Error 相当)
}
| 観点 | 標準の testing だけ | testify などを使う |
|---|---|---|
| 依存 | なし | モジュールの追加が必要 |
| 書く量 | if got != want { t.Errorf(...) } と少し長い |
assert.Equal(t, want, got) と短い |
| 失敗メッセージ | 自分で書く(自由度が高い) | 差分つきで自動表示 |
| 構造体・スライスの比較 | reflect.DeepEqual や slices.Equal / maps.Equal |
assert.Equal で差分表示 |
| チームの慣れ | Go の公式スタイルに沿う | 他言語の assert に慣れた人が読みやすい |
判断の目安: 標準ライブラリのみで始め、構造体の差分表示が欲しくなったら比較用ライブラリ(go-cmp の cmp.Diff など)、チームで assert スタイルに統一したいなら testify、と必要に応じて足すのがスムーズです。どちらを選んでも、テーブル駆動テスト・t.Run・httptest の考え方はそのまま使えます。
よくある質問
Q. t.Error と t.Fatal はどう使い分けますか?
A. 後続の確認ができなくなる場合(エラーが nil でない、ファイルが読めないなど)は t.Fatal、複数の値をまとめて比べたい場合は t.Error です。t.Fatal は呼んだテスト(またはサブテスト)だけを終了し、他のサブテストは続けて実行されます。
Q. go test の結果が (cached) になって再実行されません。
A. ソースやテストが変わっていないと結果がキャッシュされます。外部の状態に依存するテストなどで毎回実行したいときは go test -count=1 を付けます。
Q. テストファイルのパッケージ名は stock と stock_test のどちらにしますか?
A. 非公開の関数や変数もテストしたいなら同じ stock、公開 API だけを利用者の目線で確かめたいなら stock_test(外部テストパッケージ)です。1つのディレクトリに両方を置けます。本記事では Example を stock_test にしています。
Q. Go 1.23 以前の Go でも t.Context や b.Loop は使えますか?
A. 使えません。t.Context・t.Chdir・b.Loop は Go 1.24、t.ArtifactDir は Go 1.26 からです。それより前の版では context.Background() と t.Cleanup、for i := 0; i < b.N; i++ で同じことを書きます。
まとめ
- Goのテストは
xxx_test.goにTestXxx(t *testing.T)を書いてgo testで実行し、標準ライブラリだけで十分書けます - テーブル駆動テストは
name付きの表+t.Run、失敗時は名前・入力・期待値が1行でわかります - Go 1.22 以降は
tt := tt不要で、t.Parallel()をそのまま使えます(t.Setenvとの併用は不可) - ハンドラ単体は
httptest.NewRecorder、クライアント側はhttptest.NewServerと使い分けます - ベンチマークは Go 1.24 以降
for b.Loop()、ファズは「どんな入力でも成り立つ性質」を書きます go test -coverprofileとgo tool cover -htmlでカバレッジを色分け表示し、未通過の分岐を探せます- 外部ライブラリは必要になってから足すのがスムーズで、考え方は標準の書き方と共通です
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Go テストの書き方|テーブル駆動・httptestのコード例」
株式会社RJCでは、Goのテストの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。