テスト対象のコード
Goのテストの道具は、標準のツールチェーンの一部です。testing パッケージと go test コマンドです。インストールするフレームワークも、必要なアサーションのライブラリもありません。
このページの例は、タイトルをURLのスラッグに変える小さな関数 Slugify をテストします。main に手作りのチェックを付けた、実行できる形で示します。
ブラウザのエディタは main を実行するもので、go test は実行できません。以下はすべて実際のプロジェクトにある形で書き、各コマンドの後にターミナルの出力を示します。すべてGo 1.24で実行したものです。
最初のテスト
プロジェクトでは関数はパッケージの中にあり、そのテストは名前が _test.go で終わるファイルとしてその隣に置かれます。
textutil/
├── go.mod module example.com/textutil
├── slug.go package textutil, func Slugify
└── slug_test.go package textutil, the tests
// slug_test.go
package textutil
import "testing"
func TestSlugify(t *testing.T) {
got := Slugify("Hello, World!")
want := "hello-world"
if got != want {
t.Errorf("Slugify(%q) = %q, want %q", "Hello, World!", got, want)
}
}
go test が使うルールは次のとおりです。
- テストファイルになるのは
_test.goで終わるファイルだけです。go buildはそれらを無視するので、テストのコードがバイナリに入ることはありません。 - テストは、
*testing.Tを1つ受け取るTestXxxという名前の関数です(Testの後の部分は小文字で始まってはいけません)。 - テストは、失敗のメソッドを呼ぶかpanicしない限り成功します。
go test # the package in the current directory
go test ./... # every package in the module
$ go test
PASS
ok example.com/textutil 0.318s
失敗メッセージの Func(input) = got, want expected という形式はGoの慣習です。メッセージには入力を入れましょう。got "a", want "b" だけの失敗では、どの入力だったかを調べるためにコードに戻ることになります。
t.Errorfとt.Fatalf
| メソッド | 失敗にする | テストを止める |
|---|---|---|
t.Error、t.Errorf | する | 止めない、続行する |
t.Fatal、t.Fatalf | する | ただちに止める |
t.Log、t.Logf | しない | 止めない。-v か失敗時にだけ表示 |
t.Skip、t.Skipf | しない | 止める。スキップとして報告 |
1回の実行で間違ったフィールドをすべて報告できるよう、デフォルトでは Errorf を使います。テストの残りが動かないとき、典型的には想定外のエラーの後に Fatalf を使います。
func TestCountWords(t *testing.T) {
path := writeFile(t, "doc.txt", "the quick brown\nfox jumps\n")
got, err := CountWords(path)
if err != nil {
t.Fatalf("CountWords: unexpected error: %v", err) // stop: got is meaningless
}
if got != 5 {
t.Errorf("CountWords = %d, want 5", got)
}
}
Fatal は runtime.Goexit を呼んでテストを止めるので、テスト自身のゴルーチンから呼ぶ必要があります。自分で開始したゴルーチンからは、t.Error で報告して戻ります。
t.Runによるテーブル駆動テスト
Goのテストのほとんどはテーブルです。ケースのスライス、1つのループ、そして各ケースに名前を付ける t.Run です。
func TestSlugifyTable(t *testing.T) {
tests := []struct {
name, in, want string
}{
{"simple", "Go Testing", "go-testing"},
{"punctuation", "What's new in Go 1.24?", "what-s-new-in-go-1-24"},
{"extra spaces", " lots of space ", "lots-of-space"},
{"non-ascii dropped", "Café au lait", "caf-au-lait"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Slugify(tt.in); got != tt.want {
t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
ケースの追加は1行です。各サブテストは別々に報告され、その中の Fatal はそのサブテストだけを終わらせ、名前を指定して1つだけ実行することもできます。-v ですべてを実行します。
$ go test -v
=== RUN TestSlugify
--- PASS: TestSlugify (0.00s)
=== RUN TestSlugifyTable
=== RUN TestSlugifyTable/simple
=== RUN TestSlugifyTable/punctuation
=== RUN TestSlugifyTable/extra_spaces
=== RUN TestSlugifyTable/non-ascii_dropped
=== RUN TestSlugifyTable/empty
--- PASS: TestSlugifyTable (0.00s)
--- PASS: TestSlugifyTable/simple (0.00s)
--- PASS: TestSlugifyTable/punctuation (0.00s)
--- PASS: TestSlugifyTable/extra_spaces (0.00s)
--- PASS: TestSlugifyTable/non-ascii_dropped (0.00s)
--- PASS: TestSlugifyTable/empty (0.00s)
=== RUN TestCountWords
--- PASS: TestCountWords (0.00s)
=== RUN TestCountWordsMissingFile
--- PASS: TestCountWordsMissingFile (0.00s)
=== RUN ExampleSlugify
--- PASS: ExampleSlugify (0.00s)
PASS
ok example.com/textutil 0.182s
サブテスト名の空白はアンダースコアになります。ケースが失敗すると、出力はサブテストと行を示します。
--- FAIL: TestSlugifyTable (0.00s)
--- FAIL: TestSlugifyTable/underscore (0.00s)
slug_test.go:27: Slugify("snake_case_name") = "snake-case-name", want "snake_case_name"
FAIL
FAIL example.com/textutil 0.289s
Go 1.22以降はループの反復ごとに独自の tt があるので、サブテストを並列に実行しても t.Run の中のクロージャは正しいケースを見ます。古いコードではそのためにループ本体の先頭に tt := tt がよく書かれていますが、もう必要ありません。
サブテストを並列に実行するには、サブテストの関数の最初で t.Parallel() を呼びます。ケースが独立していて、効果が出るほど遅いときだけにしましょう。
よく使うgo testのフラグ
| コマンド | すること |
|---|---|
go test -v | すべてのテストの名前、結果、t.Log の出力を表示する |
go test -run TestSlugify | 名前が正規表現に一致するテストを実行する |
go test -run 'TestSlugifyTable/empty' | 1つのサブテストを実行する |
go test -count=1 | キャッシュされた結果を無視してもう一度実行する |
go test -race | データ競合検出器付きで実行する |
go test -short | 長いテストに自分でスキップさせる(if testing.Short() { t.Skip() }) |
go test -failfast | 最初に失敗したテストの後で止める |
go test -timeout 30s | 実行がそれより長くかかったら失敗にする(デフォルトは10分) |
go test -cover | 文のカバレッジを表示する |
go test -bench=. | ベンチマークも実行する |
パッケージを指定すると(go test .、go test ./...)、go test はコードと入力が変わっていないパッケージの結果をキャッシュし、その後に (cached) と表示します。引数なしの単なる go test は決してキャッシュしません。キャッシュは、テストが os パッケージを通して読むファイルと環境変数を追跡しますが、データベースやネットワークサービスのような外部の状態は追跡しません。そのため、それに依存するテストは、今実行すれば失敗するのにキャッシュから成功してしまうことがあります。-count=1 で本当に実行させます。
ヘルパー、一時ディレクトリ、後始末
上の TestCountWords の writeFile の呼び出しはテストヘルパーです。
// writeFile is a test helper: t.Helper makes failures point at the caller.
func writeFile(t *testing.T, name, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), name) // removed automatically after the test
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("writing %s: %v", name, err)
}
return path
}
t.Helper()は関数をヘルパーとしてマークするので、失敗はヘルパーの中ではなく、それを呼んだテストの行で報告されます。t.TempDir()は新しいディレクトリを作り、テストが終わると削除します。テストが自分でファイルを後始末する必要はありません。t.Cleanup(func() { ... })はその他の後処理(サーバーを閉じる、テーブルを削除する)を登録します。後始末はテストとそのサブテストの後に、最後に登録したものから実行されます。t.Setenv("KEY", "value")は、このテストのためだけに環境変数を設定し、後で元に戻します。t.Context()(Go 1.24)は、後始末が実行される直前にキャンセルされるコンテキストを返します。
テストのフィクスチャのファイルは、テストの隣の testdata という名前のディレクトリに置きます。go ツールはそれをパッケージとしては無視し、テストはパッケージのディレクトリを作業ディレクトリとして実行されるので、os.ReadFile("testdata/input.json") が動きます。
エラーとHTTPハンドラのテスト
エラーは、呼び出し側のコードと同じように errors.Is や errors.As でチェックします。
func TestCountWordsMissingFile(t *testing.T) {
_, err := CountWords(filepath.Join(t.TempDir(), "nope.txt"))
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("err = %v, want fs.ErrNotExist", err)
}
}
エラーの文字列を比較すると、誰かがメッセージを言い換えた途端に壊れます。
HTTPハンドラには、net/http/httptest が偽の ResponseWriter を提供してくれるので、ネットワークなしにハンドラを直接呼べます。
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
}
if body := rec.Body.String(); body != "ok\n" {
t.Errorf("body = %q, want %q", body, "ok\n")
}
}
httptest.NewServer は、クライアントのコードをテストするためにlocalhostで本物のサーバーを開始します。HTTPクライアントのページでは、すべての例でこれを使っています。
カバレッジ
$ go test -cover
PASS
coverage: 100.0% of statements
ok example.com/textutil 0.578s
$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
example.com/textutil/slug.go:11: Slugify 100.0%
example.com/textutil/words.go:9: CountWords 100.0%
total: (statements) 100.0%
$ go tool cover -html=cover.out # opens a browser with covered lines in green
カバレッジは一度も実行されなかった行を教えてくれるので、テストされていない分岐を見つけるのに役立ちます。数値が高くても、アサーションが良いものだとは限りません。すべての関数を呼んで何もチェックしないテストでも100%に達します。
ベンチマーク
ベンチマークは func BenchmarkXxx(b *testing.B) です。Go 1.24で b.Loop が追加され、今はこれが推奨される形です。
func BenchmarkSlugify(b *testing.B) {
for b.Loop() {
Slugify("The Go Programming Language, 2nd Edition")
}
}
b.Loop は安定した計測に必要なだけ本体を実行し、ループの前の準備のコードを計測から除き、コンパイラが呼び出しを最適化で消してしまうのを防ぎます。Go 1.24より前に書かれたコードは for i := 0; i < b.N; i++ を使っています。今でも動きますが、重い準備の後には b.ResetTimer() が必要で、デッドコード削除にだまされることもあります。
ベンチマークは単なる go test では実行されません。明示的に頼み、通常のテストは -run='^$' で飛ばします。
$ go test -bench=. -benchmem -run='^$'
goos: darwin
goarch: arm64
pkg: example.com/textutil
cpu: Apple M4
BenchmarkSlugify-10 5061945 238.1 ns/op 168 B/op 5 allocs/op
PASS
ok example.com/textutil 1.410s
列は、GOMAXPROCS が付いた名前、実行した反復回数、1回の呼び出しの時間、1回の呼び出しで確保したバイト数、1回の呼び出しのメモリ確保の回数です。数値はマシンに依存するので、同じマシンでの実行どうしを比べます。変更の前後を確実に比べるには、それぞれを -count=10 で数回実行し、両方の出力を benchstat(golang.org/x/perf/cmd/benchstat)に渡します。
Exampleもテスト
Example関数は何かを表示し、期待する出力をコメントで宣言します。go test はそれを実行して出力が違えば失敗させ、go doc とpkg.go.devはそれをドキュメントとして表示します。
// example_test.go
package textutil_test
import (
"fmt"
"example.com/textutil"
)
func ExampleSlugify() {
fmt.Println(textutil.Slugify("Hello, World!"))
// Output: hello-world
}
パッケージ名 textutil_test により、これは外部テストになります。実際の呼び出し側と同じように、公開されたAPIしか使えません。このようなファイルはパッケージと同じディレクトリに置けます。// Output: のコメントがないExampleは、コンパイルされますが実行されません。行がどの順で現れてもよいときは // Unordered output: を使います。
ファジングを少しだけ
Go 1.18でファズテストが追加されました。入力を生成して、クラッシュや壊れた不変条件を見つけます。
func FuzzSlugify(f *testing.F) {
f.Add("Hello, World!") // seed input
f.Fuzz(func(t *testing.T, s string) {
slug := Slugify(s)
if strings.Contains(slug, "--") || strings.HasPrefix(slug, "-") {
t.Errorf("Slugify(%q) = %q: bad hyphens", s, slug)
}
})
}
単なる go test は、シードの入力(f.Add の値と testdata/fuzz に保存されたファイル)だけを実行します。go test -fuzz=FuzzSlugify は、失敗を見つけるか止められるまで新しい入力を生成し続け、失敗した入力を testdata/fuzz に保存するので、それが通常のテストケースになります。
よくある間違い
- テストファイルの場所や名前が間違っている。
_test.goで終わり、パッケージのディレクトリにある必要があります。slug_tests.goはテストとして実行されず、パッケージにコンパイルされます。 Testの後が小文字。Testslugifyはテストではありません。TestSlugifyとTest_slugifyはテストです。- 別のゴルーチンからの
t.Fatal。 そこではt.Errorで報告し、テストが戻る前にゴルーチンを待ちます。 - 入力のないメッセージ。 何を渡し、何が出てきて、何を期待したかを含めます。
- キャッシュされた成功を信じる。 テストがパッケージの外の何かに依存するなら
-count=1を使います。 - 互いの順序や共有のグローバル変数に依存するテスト。 各テストは自分の状態を自分で準備するべきで、
t.TempDir、t.Setenv、t.Cleanupはそのためにあります。
よくある質問
Goでユニットテストはどう書きますか?
コードと同じディレクトリに _test.go で終わるファイルを作り、testing をインポートし、コードを呼んで不一致を t.Errorf で報告する関数 func TestName(t *testing.T) を書きます。そのディレクトリで go test を、モジュール全体なら go test ./... を実行します。フレームワークもアサーションのライブラリも要りません。
Goのt.Errorとt.Fatalの違いは何ですか?
t.Error と t.Errorf はテストを失敗としてマークし、実行を続けるので、1回の実行で複数の問題を報告できます。t.Fatal と t.Fatalf は失敗としてマークし、ただちにテストを止めます。結果が使えなくなる想定外のエラーの後のように、続けても意味がないときに Fatal を使います。
Goでテストを1つだけ実行するには?
-run に正規表現を渡します。go test -run TestSlugify は名前が一致するすべてのテストを実行し、go test -run 'TestSlugifyTable/punctuation' は1つのサブテストを実行します(サブテスト名の空白はアンダースコアになります)。各テストの名前と結果を見るには -v を、テストのキャッシュを回避するには -count=1 を加えます。
Goでベンチマークはどう書きますか?
_test.go ファイルに func BenchmarkName(b *testing.B) を書き、計測するコードを for b.Loop() { ... } のループに入れます(Go 1.24。古いコードでは for i := 0; i < b.N; i++)。go test -bench=. -benchmem で実行すると、1操作あたりのナノ秒、バイト数、メモリ確保の回数が報告されます。
Goでテストのカバレッジを見るには?
go test -cover は、テストが実行した文の割合を表示します。詳しく見るには go test -coverprofile=cover.out でプロファイルを書き出し、関数ごとの数値なら go tool cover -func=cover.out、カバーされた行とされていない行をブラウザで見るなら go tool cover -html=cover.out を実行します。