Menu

Go言語のテスト:ユニットテスト、テーブル駆動テスト、ベンチマーク

標準のtestingパッケージとgo testでGoのコードをテストする方法を解説します。_test.goファイル、TestXxx関数、t.Errorfとt.Fatalfの違い、t.Runによるテーブル駆動テスト、ヘルパーと一時ディレクトリ、カバレッジ、b.Loopによるベンチマーク、Exampleテストまで。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

テスト対象のコード

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.Errort.Errorfする止めない、続行する
t.Fatalt.Fatalfするただちに止める
t.Logt.Logfしない止めない。-v か失敗時にだけ表示
t.Skipt.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)
	}
}

Fatalruntime.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 で本当に実行させます。

ヘルパー、一時ディレクトリ、後始末

上の TestCountWordswriteFile の呼び出しはテストヘルパーです。

// 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.Iserrors.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 で数回実行し、両方の出力を benchstatgolang.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 はテストではありません。TestSlugifyTest_slugify はテストです。
  • 別のゴルーチンからの t.Fatal そこでは t.Error で報告し、テストが戻る前にゴルーチンを待ちます。
  • 入力のないメッセージ。 何を渡し、何が出てきて、何を期待したかを含めます。
  • キャッシュされた成功を信じる。 テストがパッケージの外の何かに依存するなら -count=1 を使います。
  • 互いの順序や共有のグローバル変数に依存するテスト。 各テストは自分の状態を自分で準備するべきで、t.TempDirt.Setenvt.Cleanup はそのためにあります。

よくある質問

Goでユニットテストはどう書きますか?

コードと同じディレクトリに _test.go で終わるファイルを作り、testing をインポートし、コードを呼んで不一致を t.Errorf で報告する関数 func TestName(t *testing.T) を書きます。そのディレクトリで go test を、モジュール全体なら go test ./... を実行します。フレームワークもアサーションのライブラリも要りません。

Goのt.Errorとt.Fatalの違いは何ですか?

t.Errort.Errorf はテストを失敗としてマークし、実行を続けるので、1回の実行で複数の問題を報告できます。t.Fatalt.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 を実行します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める