はじめに 製造エネルギー事業部の辻です。Goのテストをはじめてみよう、という記事です。
この記事は、5年前の2020年に私が執筆したGoのテスト入門記事(Goのテストに入門してみよう! )のリメイク版です。当時は執筆したときのGoのバージョンは1.14でした。記事はありがたいことに継続的に反響をいただいていたものの、いくつか記述が古くなっていた点がありました。そこで今回Go1.15以降で導入された機能や、周辺のアップデート等を取り込み、改良しました。
Go のテストに関するヒント集としてお役に立てれば幸いです。
Tips Go のテストの仕組みに、ベンチマークに関するテストと Example テストというサンプルコード用のテストも含まれているのですが、この2つは対象外にします。基礎的と思われる内容から順に並べてみました。
テストがしたい Goのテストは go test
コマンドを用いてテストを実施します。テストを実施する関数の命名は以下のような形式でなければなりません。
TestXxx
は Testxxx
ではダメです。Test_xxx
という関数名であれば問題ありません。
テストファイルは xxxx_test.go
といった命名である必要があります。このファイルはビルド時には除かれます。簡単なテストを試してみます。
main_test.go package mainimport "testing" func add (a, b int ) int { return a + b } func TestAdd (t *testing.T) { type args struct { a int b int } tests := []struct { name string args args want int }{ { name: "normal" , args: args{a: 1 , b: 2 }, want: 3 , }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { if got := add(tt.args.a, tt.args.b); got != tt.want { t.Errorf("add() = %v, want %v" , got, tt.want) } }) } }
https://play.golang.org/p/XYrpmljtPrW
$ go test PASS ok sample 0.355s
上記のテストは TableDrivenTest とサブテストを組み合わせています。どちらもGoの開発現場でよく使われます。TableDrivenTests については公式のWikiも合わせて参照ください。またサブテストを用いると各テストごとに結果がわかるようになります。TableDrivenTest はさまざまな Input/Output パターンを網羅するのに便利です。参考までに上記のテストをTableDrivenTestを使わずに書き直すと以下のようになります。
func add (a, b int ) int { return a + b } func TestAdd (t *testing.T) { if add(1 , 2 ) != 3 { t.Errorf("add() = %v, want %v" , add(1 , 2 ), 3 ) } }
https://play.golang.org/p/-TQObdTOqPA
テストの失敗をレポートしたい Go はテストのアサーションを提供していません。理由は公式のFAQ で紹介されています。先程の TestAdd()
関数のように、テストが失敗したことを開発者自ら実装する必要があります。失敗したことを示すには T.Error()
や T.Fatal()
を利用できます。
T.Fatal()
を用いると T.Fatal()
が実行された以降のテストは呼び出されずに終了します。テストが失敗したことを示すには T.Error()
を使い、処理が失敗するとその後のテストが無意味になる場合は T.Fatal()
を用いると良いでしょう。以下のように t.Fatal()
を用いた場合は、その後のテストの処理 t.Log("after add() ...")
が呼び出されていないことが分かります。ただし defer
や T.Cleanup()
といった後処理は呼び出されます。
func TestAdd (t *testing.T) { type args struct { a int b int } tests := []struct { name string args args want int }{ {name: "fail" , args: args{a: 1 , b: 2 }, want: 30 }, {name: "normal" , args: args{a: 1 , b: 2 }, want: 3 }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { t.Cleanup(func () { t.Log("cleanup!" ) }) defer t.Log("defer!" ) if got := add(tt.args.a, tt.args.b); got != tt.want { t.Fatalf("add() = %v, want %v" , got, tt.want) } t.Log("after add() ..." ) }) } }
=== RUN TestAdd === RUN TestAdd/fail prog_test.go:31: add() = 3, want 30 panic.go:636: defer! prog_test.go:24: cleanup! === RUN TestAdd/normal prog_test.go:34: after add() ... prog_test.go:35: defer! prog_test.go:24: cleanup! --- FAIL: TestAdd (0.00s) --- FAIL: TestAdd/fail (0.00s) --- PASS: TestAdd/normal (0.00s) FAIL
https://play.golang.org/p/2NZxz45zGD7
なお t.Fatalf
と似たような関数名でログ出力してアプリケーションを終了する log
パッケージの Fatalf
という関数があります。log.Fatalf
のGo Docにもあるように log.Fatalf
は defer
といった後処理を呼び出さず、即座に os.Exit(1)
でアプリケーションが終了します。t.Fatalf
と log.Fatalf
を混乱しないように注意しましょう。
https://play.golang.org/p/ADm5xU7gsfm
テストをスキップしたい 時間を要するテストがあり、何らかの場合において、テストをスキップしたい場合があるかもしれません。次のような処理にめちゃくちゃ時間がかかる例を考えてみます。
func add (a, b int ) int { time.Sleep(365 * 24 * time.Hour) return a + b }
このテストをスキップしたいとします。その場合は T.Skip()
というメソッドを利用できます。
テストコードに以下を追加します。
b: 2 , }, want: 3 , }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode." ) } if got := add(tt.args.a, tt.args.b); got != tt.want { t.Errorf("add() = %v, want %v" , got, tt.want) } }) }
$ go test -v -short === RUN Test_add === RUN Test_add/normal_1 --- PASS: Test_add (0.00s) --- SKIP: Test_add/normal_1 (0.00s) main_test.go:35: skipping test in short mode. PASS ok github.com/d-tsuji/go-sandbox 0.339s
テストがスキップされていることが分かります。さらっと testing.Short()
という関数も用いましたが Short()
は testing
パッケージに含まれている関数で、-short
フラグがセットされていると true になります。そのためテストを実施するときに -short
というフラグを付与したときだけテストがスキップされる、そうでないときはスキップされずテストが実施される、というように使い分けることができます。また -v
オプションをつけてテスト実行することでサブテスト含め、実行したすべてのテスト結果を出力できます。
標準パッケージのテストにおいても、いくつかスキップしているケースを確認できます。以下は io/ioutil/ioutil_test.go
からの抜粋です。特定の条件を満たす場合にテストをスキップするように実装されています。
io/ioutil/ioutil_test.go func TestReadOnlyWriteFile (t *testing.T) { if os.Getuid() == 0 { t.Skipf("Root can write to read-only files anyway, so skip the read-only test." ) }
テストを並列に実施したい 以下のような謎に sleep する実装があったとしましょう。テストを効率的に実行するために並列化を試みます。
func add (a, b int ) int { time.Sleep(time.Duration(a+b) * time.Second) return a + b }
以下のサブテストを逐次実施すると 3 + 5 + 7 = 15 から約15秒テスト実施に時間がかかります。
func Test_add (t *testing.T) { type args struct { a int b int } tests := []struct { name string args args want int }{ {"normal_1" , args{1 , 2 }, 3 }, {"normal_2" , args{2 , 3 }, 5 }, {"normal_3" , args{3 , 4 }, 7 }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { if got := add(tt.args.a, tt.args.b); got != tt.want { t.Errorf("add() = %v, want %v" , got, tt.want) } }) } }
$ go test -v === RUN Test_add === RUN Test_add/normal_1 === RUN Test_add/normal_2 === RUN Test_add/normal_3 --- PASS: Test_add (15.01s) --- PASS: Test_add/normal_1 (3.00s) --- PASS: Test_add/normal_2 (5.00s) --- PASS: Test_add/normal_3 (7.00s) PASS ok github.com/d-tsuji/go-sandbox 15.315s
並列にテストを実施するには T.Parallel()
メソッドが利用できます。
for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { t.Parallel() if got := add(tt.args.a, tt.args.b); got != tt.want { t.Errorf("add() = %v, want %v" , got, tt.want) } }) }
テストが並列に実施されていることが分かります。結果としてテストにかかった時間は約7秒になりました。
$ go test -v === RUN Test_add === RUN Test_add/normal_1 === PAUSE Test_add/normal_1 === RUN Test_add/normal_2 === PAUSE Test_add/normal_2 === RUN Test_add/normal_3 === PAUSE Test_add/normal_3 === CONT Test_add/normal_1 === CONT Test_add/normal_3 === CONT Test_add/normal_2 --- PASS: Test_add (0.00s) --- PASS: Test_add/normal_1 (3.00s) --- PASS: Test_add/normal_2 (5.00s) --- PASS: Test_add/normal_3 (7.00s) PASS ok github.com/d-tsuji/go-sandbox 7.317s
Go 1.21以前はループ内で t.Parallel()
で並列化するときなどにおいて、変数への参照が競合しないように以下の tt := tt
といったメモリをループの内側で再確保する実装が必要でした。
Go1.22からはGo側でループごとに変数が作成されるようになったため、ユーザー側で tt := tt
などとする必要はなくなりました。
Go1.21以前の書き方 for _, tt := range tests { tt := tt t.Run(tt.name, func (t *testing.T) { t.Parallel() if got := add(tt.args.a, tt.args.b); got != tt.want { t.Errorf("add() = %v, want %v" , got, tt.want) } }) }
なお、テストケース自体がそもそも並列に実行できない場合、例えばデータベース上のテーブルへの UPDATE や INSERT が発生し、テストケースで競合する場合、テストを並列に実行することはできないため、注意が必要です。
テストの前処理や後処理を実施したい テスト実行にあたって、設定ファイルの読み込み・初期状態に戻すなどといった前処理や後処理をしたい場合があると思います。そのような共通的な前処理や後処理を実施したい場合に TestMain()
関数を利用できます。
例を見てみます。
func f () { fmt.Println("なんらかの処理" ) }
TestMain()
を用いて、テストの前後に処理を実行できます。
func Test_f (t *testing.T) { f() } func TestMain (m *testing.M) { fmt.Println("前処理" ) m.Run() fmt.Println("後処理" ) }
Test_f()
の実行前後で前処理と後処理が実行されていることが分かります。
前処理 === RUN Test_f なんらかの処理 --- PASS: Test_f (0.00s) PASS 後処理
https://go.dev/play/p/2JbCk_jtPxw
Go 1.14 以前は TestMain()
を利用する際に m.Run()
の戻り値であるステータスコードを os.Exit()
に渡す必要がありました。Go1.15 以降はユーザー側で os.Exit()
を呼び出す必要はなくなりました。
Go1.14以前の書き方 func Test_f (t *testing.T) { f() } func TestMain (m *testing.M) { fmt.Println("前処理" ) status := m.Run() fmt.Println("後処理" ) os.Exit(status) }
また、後処理には T.Cleanup()
が便利です。リソースの後処理という意味では defer
を利用すればよいのでは? と思った方もいるかもしれません。大きな違いとして実行タイミングが挙げられます。
T.Cleanup()
を便利に使えることが実感できるシーンの1つとして、テストに必要な前処理をテストとは別の関数で実施している場合があります。
簡単な例ですが、テストの前準備としてテスト用のファイルを生成する必要があったとして、テスト終了後に削除したい場合、以下のような実装が考えられます。TempFile()
関数ではリソースをクローズするコールバック関数を呼び出し元に返却する必要があり、呼び出し元で後処理としてコールバック関数を呼び出します。
testutil/file.go func TempFile (t *testing.T) (*os.File, func () ) { t.Helper() file, err := os.CreateTemp("" , "test" ) if err != nil { t.Fatal(err) } cleanup := func () { if err := os.Remove(file.Name()); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } } return file, cleanup }
呼び出し元の処理です。
file, cleanup := testutil.TempFile(t) defer cleanup()
T.Cleanup()
を用いると前処理を実施する関数内でリソースの後処理が実施できるようになります。TempFile()
関数の例であれば、以下のように t.Cleanup()
を利用できます。関数から return したタイミングで呼び出される defer
とは異なり、テストが完了したタイミングで T.Cleanup()
処理が呼び出されます。
func TempFile (t *testing.T) *os.File { t.Helper() file, err := os.CreateTemp("" , "test" ) if err != nil { t.Fatal(err) } t.Cleanup(func () { if err := os.Remove(file.Name()); err != nil { t.Fatal(err) } if err := file.Close(); err != nil { t.Fatal(err) } }) return file }
ファイルでなくても、テスト用のデータベースや HTTP サーバの teardown など、テストに必要なリソースの teardown 処理を同じ関数内に記述でき、テストコードを簡潔にできます。
カバレッジを取得したい 標準パッケージのカバレッジを取得してみます。今回は io
パッケージ( ioutil
も含む)のテストのカバレッジを取得することにします。カバレッジは go test
コマンドの引数として -covermode=count
を付与すれば取得できます。 -coverprofile=c.out
で結果をファイルに保存します。
Go のコードは /Go/src
にあるものとします。以下のコマンドを実行してカバレッジを取得します。
$ go test io/... -covermode=count -coverprofile=c.out ok io 0.620s coverage: 95.2% of statements ok io/ioutil 0.311s coverage: 68.9% of statements
以下のコマンドで上記の出力ファイルを簡単に HTML で可視化できます。
go tool cover -html=c.out -o coverage.html
このような感じでどのパスが通っていないか確認できます。
環境変数を設定したい 単体テスト内のみで利用できる一時的な環境変数の設定に T.Setenv()
を用いることができます。環境変数は os.Setenv()
関数でも設定できますが、os.Setenv()
で設定した環境変数はプロセスの生存期間全体にわたって設定されます。あるテストのみで設定したい場合に T.Setenv()
が有用です。ただし t.Parallel()
を利用した並列テストでは利用できない点に注意が必要です。
以下はGo本体で利用されているテストコード です。
crypto/rsa/boring_test.go func TestBoringASN1Marshal (t *testing.T) { t.Setenv("GODEBUG" , "rsa1024min=0" ) k, err := GenerateKey(rand.Reader, 128 ) if err != nil { t.Fatal(err) } _, err = asn1.Marshal(k.PublicKey) if err != nil { t.Fatal(err) } }
T.Setenv()
はGo1.17で導入されました。Go1.16以前は以下のようなコードが慣習的でした。
テストで設定した環境変数を削除する func TestSomething (t *testing.T) { os.Setenv("KEY" , "VALUE" ) defer os.Unsetenv("ENV" ) }
テスト前に設定されていた環境変数に戻す func TestSomething (t *testing.T) { org := os.Getenv("KEY" ) os.Setenv("KEY" , "VALUE" ) defer os.Setenv("KEY" , org) }
あるディレクトリ配下のテストをすべて実施したい たとえば標準パッケージの例だと、io
パッケージはテスト対象に含めるが、その他のパッケージはテスト対象に含めない…といった要領です。これはテストのコマンドというよりはパッケージのコマンドになります。以下のように ...
の文字列を用いてワイルドカードとしてテスト対象のファイルを選択できます。詳細は go help packages
とすることで確認できます。
以下の場合は io
パッケージに含まれるすべてのテストを実行します。
同様に io
パッケージに含まれる ioutil
パッケージのみテストしたい場合は以下のようになります。
$ go test -v io/ioutil/... === RUN TestReadFile --- PASS: TestReadFile (0.00s) === RUN TestWriteFile --- PASS: TestWriteFile (0.00s) === RUN TestReadDir --- PASS: TestReadDir (0.00s) === RUN TestTempFile --- PASS: TestTempFile (0.00s) === RUN TestTempFile_pattern --- PASS: TestTempFile_pattern (0.00s) === RUN TestTempDir --- PASS: TestTempDir (0.00s) === RUN TestTempDir_BadDir --- PASS: TestTempDir_BadDir (0.00s) === RUN ExampleReadAll --- PASS: ExampleReadAll (0.00s) === RUN ExampleReadFile --- PASS: ExampleReadFile (0.00s) PASS ok io/ioutil 0.342s
一部のテストケースのみ実施したい 実行対象のテストを抽出するには -run
フラグを用いることできます。以下のように正規表現を用いて、一致するテストのみを実行できます。
例えば io
パッケージで Pipe に関するテストのみ実行したいとしましょう。Pipe に関するテストはテスト関数に Pipe
の文字列が含まれるものとします。以下のように実行するとテストの関数名に Pipe
が含まれるテストのみ実行されていることが分かります。
go test -v io/... -run Pipe === RUN TestPipe1 --- PASS: TestPipe1 (0.00s) === RUN TestPipe2 --- PASS: TestPipe2 (0.00s) === RUN TestPipe3 --- PASS: TestPipe3 (0.00s) === RUN TestPipeReadClose --- PASS: TestPipeReadClose (0.01s) === RUN TestPipeReadClose2 --- PASS: TestPipeReadClose2 (0.00s) === RUN TestPipeWriteClose --- PASS: TestPipeWriteClose (0.01s) === RUN TestPipeWriteClose2 --- PASS: TestPipeWriteClose2 (0.00s) === RUN TestPipeCloseError --- PASS: TestPipeCloseError (0.00s) === RUN TestPipeConcurrent === RUN TestPipeConcurrent/Write === RUN TestPipeConcurrent/Read --- PASS: TestPipeConcurrent (0.00s) --- PASS: TestPipeConcurrent/Write (0.00s) --- PASS: TestPipeConcurrent/Read (0.00s) === RUN ExamplePipe --- PASS: ExamplePipe (0.00s) PASS ok io 0.345s testing: warning: no tests to run PASS ok io/ioutil 0.575s [no tests to run]
サブテストの一部のみ実施したい TableDrivenTest でテストを実装していると、ある1つのテストケースで複数のサブテストを実装します。サブテストの一部のみテストを実行したい場合はテストケースを絞り込むときと同様に -run
フラグを用いることができます。-run=X/Y
などとすると X にマッチするテストの Y にマッチするサブテストを実行できます。
func TestAdd (t *testing.T) { type args struct { a int b int } tests := []struct { name string args args want int }{ {name: "fail" , args: args{a: 1 , b: 2 }, want: 30 }, {name: "normal" , args: args{a: 1 , b: 2 }, want: 3 }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { if got := add(tt.args.a, tt.args.b); got != tt.want { t.Errorf("add() = %v, want %v" , got, tt.want) } }) } }
-run Add/mal
とすると TestAdd()
の normal
のサブテストのみ実行されていることが分かります。サブテストがたくさんある場合で、一部のみ実行したい場合に便利です。
$ go test -v -run Add/mal === RUN TestAdd === RUN TestAdd/normal --- PASS: TestAdd (0.00s) --- PASS: TestAdd/normal (0.00s) PASS ok github.com/d-tsuji/go-sandbox 0.181s
テストのキャッシュを削除したい Go でテストをしていると以下のように cached
の文字列を見ることがあると思います。テストのキャッシュを削除するにはどのようにすれば良いでしょうか。
$ go test io/... -run Pipe ok io (cached) ok io/ioutil (cached) [no tests to run]
テストのキャッシュは Go1.10 から組み込まれました。テストのキャッシュの詳細は go1.10#test が詳しいです。キャッシュ使わない場合はを明示的に -count=1
と指定すればよいです。-count=1
と明示的に指定するとテストはキャッシュされなくなります。
go test io/... -run Pipe -count=1 ok io 0.304 s ok io/ioutil 0.563 s [no tests to run] ... go test io/... -run Pipe -count=1 ok io 0.314 s ok io/ioutil 0.576 s [no tests to run] ... go test io/... -run Pipe -count=1 ok io 0.321 s ok io/ioutil 0.575 s [no tests to run]
また go clean -cache
を用いてもビルドキャッシュ全体を削除でき、テストのキャッシュも削除できます。
一時ディレクトリを作成したい ファイルの読み書きに関するテストなどで、一時的なディレクトリやファイルを生成したいときがあります。Go1.15 から T.TempDir()
というテスト時のみで利用できる一時ディレクトリを作成する機能が追加になりました。T.TempDir()
ではテスト終了時にディレクトリとそのディレクトリ配下のファイルは削除されます。
T.TempDir()
のサンプルコードを記載します。
func TestTempDir (t *testing.T) { tmpDir := t.TempDir() f := filepath.Join(tmpDir, "sample.txt" ) err := os.WriteFile(f, []byte ("hoge" ), 0666 ) if err != nil { t.Fatal(err) } t.Logf("tmpFilePath = %s" , f) }
=== RUN TestTempDir scratch_test.go:18: tmpFilePath = /tmp/TestTempDir3383921458/001/sample.txt --- PASS: TestTempDir (0.00s) PASS ok sample 0.003s
基本的には T.TempDir()
で作成したディレクトリにファイルを作成してテストするのが良いでしょう。ただしテスト終了時に一時ディレクトリが削除されては困る、みたいな場合は T.TempDir()
は利用できません。その場合は Goのテストでファイルの読み書きを扱いたい で詳しく解説していますので、こちらの記事を参照ください。
テストコードの雛形を楽に作りたい 小ネタですが IDE などの機能を使うと少し便利になるかもしれません。例えば GoLand の機能を用いるとデフォルトで TableDrivenTest の雛形を生成してくれます。詳細は割愛しますが VS Code を用いて開発する場合でも同様に雛形を生成できます。
構造体、マップやスライスの比較を実施したい map
のキーと値が一致しているか確認するようなテストがしたいとしましょう。map
や slice
は spec#Comparison_operators にもあるように比較演算子を用いて比較できません。map
に含まれるキーと値が同じものを含んでいるかどうかはループを回して確認しないといけないのでしょうか。
reflect
パッケージに含まれる reflect.DeepEqual
を用いると map のような比較演算子で比較できないオブジェクトの比較ができます。
net/http
パッケージにある request_test.go
の中でクエリパラメータなどに用いられる map
(map[string][]string
型)は以下のように比較して同値かどうかテストしています。
net/http/request_test.go wantForm := url.Values{ "language" : []string {"Go" }, "name" : []string {"gopher" }, "skill" : []string {"go-ing" }, "field1" : []string {"value1" }, "field2" : []string {"initial-value2" , "value2" }, } if !reflect.DeepEqual(req.Form, wantForm) { t.Fatalf("req.Form = %v, want %v" , req.Form, wantForm) }
https://github.com/golang/go/blob/49860cf92a9a3ba434d2bc393faaefabe48d181e/src/net/http/request_test.go#L231-L240
Go は言語自体にテスティングフレームワークを提供してません。Where is my favorite helper function for testing? に理由が記載されていますが、端的に言うと開発者が適切にエラーハンドリングすることと適切なエラーメッセージの出力を実装することが重要であるためです。
とはいえ、薄い便利ライブラリを使いたくなるシチュエーションはあるでしょう。reflect.DeepEqual()
を使った同値チェックは google/go-cmp を使うとより便利にテストができます。私は reflect.DeepEqual()
の代わりとして google/go-cmp
を用いることが多いです。
go-cmp
のリポジトリにある構造体を比較するサンプルの例を見てみます。以下のような net.IP
型や time.Time
型をフィールドに持つ構造体の比較です。
example_test.go type ( Gateway struct { SSID string IPAddress net.IP NetMask net.IPMask Clients []Client } Client struct { Hostname string IPAddress net.IP LastSeen time.Time } ) func MakeGatewayInfo () (x, y Gateway) { x = Gateway{ SSID: "CoffeeShopWiFi" , IPAddress: net.IPv4(192 , 168 , 0 , 1 ), NetMask: net.IPv4Mask(255 , 255 , 0 , 0 ), Clients: []Client{{ Hostname: "ristretto" , IPAddress: net.IPv4(192 , 168 , 0 , 116 ), }, { Hostname: "aribica" , IPAddress: net.IPv4(192 , 168 , 0 , 104 ), LastSeen: time.Date(2009 , time.November, 10 , 23 , 6 , 32 , 0 , time.UTC), }, { Hostname: "macchiato" , IPAddress: net.IPv4(192 , 168 , 0 , 153 ), LastSeen: time.Date(2009 , time.November, 10 , 23 , 39 , 43 , 0 , time.UTC), }, { Hostname: "espresso" , IPAddress: net.IPv4(192 , 168 , 0 , 121 ), }, { Hostname: "latte" , IPAddress: net.IPv4(192 , 168 , 0 , 219 ), LastSeen: time.Date(2009 , time.November, 10 , 23 , 0 , 23 , 0 , time.UTC), }, { Hostname: "americano" , IPAddress: net.IPv4(192 , 168 , 0 , 188 ), LastSeen: time.Date(2009 , time.November, 10 , 23 , 3 , 5 , 0 , time.UTC), }}, } y = Gateway{ SSID: "CoffeeShopWiFi" , IPAddress: net.IPv4(192 , 168 , 0 , 2 ), NetMask: net.IPv4Mask(255 , 255 , 0 , 0 ), Clients: []Client{{ Hostname: "ristretto" , IPAddress: net.IPv4(192 , 168 , 0 , 116 ), }, { Hostname: "aribica" , IPAddress: net.IPv4(192 , 168 , 0 , 104 ), LastSeen: time.Date(2009 , time.November, 10 , 23 , 6 , 32 , 0 , time.UTC), }, { Hostname: "macchiato" , IPAddress: net.IPv4(192 , 168 , 0 , 153 ), LastSeen: time.Date(2009 , time.November, 10 , 23 , 39 , 43 , 0 , time.UTC), }, { Hostname: "espresso" , IPAddress: net.IPv4(192 , 168 , 0 , 121 ), }, { Hostname: "latte" , IPAddress: net.IPv4(192 , 168 , 0 , 221 ), LastSeen: time.Date(2009 , time.November, 10 , 23 , 0 , 23 , 0 , time.UTC), }}, } return x, y }
この構造体が同値かテストしてみます。上記の実装の通り MakeGatewayInfo
は異なる 2 つの変数 x
と y
を返す関数です。これを go-cmp
と reflect.DeepEqual()
のそれぞれを用いて比較してみます。
example_test.go func TestMakeGatewayInfoDeepEqual (t *testing.T) { got, want := MakeGatewayInfo() if !reflect.DeepEqual(got, want) { t.Errorf("MakeGatewayInfo() got = %v, want %v" , got, want) } } func TestMakeGatewayInfoGoCmp (t *testing.T) { got, want := MakeGatewayInfo() if diff := cmp.Diff(want, got); diff != "" { t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s" , diff) } }
実行すると以下のような結果を得ることができます。
$ go test example_test.go --- FAIL: TestMakeGatewayInfoDeepEqual (0.00s) example_test.go:15: MakeGatewayInfo() got = {CoffeeShopWiFi 192.168.0.1 ffff0000 [{ristretto 192.168.0.116 0001-01-01 00:00:00 +0000 UTC} {aribica 192.168.0.104 2009-11-10 23:06:32 +0000 UTC} {macchiato 192.168.0.153 2009-11-10 23:39:43 +0000 UTC} {espresso 192 .168.0.121 0001-01-01 00:00:00 +0000 UTC} {latte 192.168.0.219 2009-11-10 23:00:23 +0000 UTC} {americano 192.168.0.188 2009-11-10 23:03:05 +0000 UTC}]}, want {CoffeeShopWiFi 192.168.0.2 ffff0000 [{ristretto 192.168.0.116 0001-01-01 00:00:00 +0000 UTC} {aribica 192. 168.0.104 2009-11-10 23:06:32 +0000 UTC} {macchiato 192.168.0.153 2009-11-10 23:39:43 +0000 UTC} {espresso 192.168.0.121 0001-01-01 00:00:00 +0000 UTC} {latte 192.168.0.221 2009-11-10 23:00:23 +0000 UTC}]} --- FAIL: TestMakeGatewayInfoGoCmp (0.00s) example_test.go:22: MakeGatewayInfo() mismatch (-want +got): {t.Gateway}.IPAddress: -: s"192.168.0.2" +: s"192.168.0.1" {t.Gateway}.Clients[4].IPAddress: -: s"192.168.0.221" +: s"192.168.0.219" {t.Gateway}.Clients[?->5]: -: <non-existent> +: t.Client{Hostname: "americano" , IPAddress: s"192.168.0.188" , LastSeen: s"2009-11-10 23:03:05 +0000 UTC" } FAIL FAIL command-line-arguments 0.304s FAIL
go-cmp
の利点の1つとして、テストの出力から、何が同値で、何が同値でなかったか、同値でなかったときは取得した値と想定する値は何であったか明示的にわかる点が挙げられます。他にもオプションで条件をカスタマイズできます。
便利ライブラリに関しては社内でも stretchr/testify/assert
などのライブラリを使うGopherを見かけます。Go のテスティングライブラリに関しては好みが分かれるところだと思うので、深くは触れません。
APIサーバにアクセスするテストをしたい net/http/httptest
を利用すると簡単にテスト用のモックサーバを用意できます。リクエストに対して Hello, client
というレスポンスを返却する API を用いた、API クライアントの視点で単体テストを実施する例を考えてみます。
httptest.NewServer
にハンドラを渡すことでハンドラの振る舞いをするローカルの HTTP サーバを提供してくれます。デフォルトだとこのテスト用の HTTP サーバは 127.0.0.1:0
で起動します。ポートが 0
にバインドされていますが、これは空いている任意ポートを割り当てます。なお、ブラウザからデバッグしたいなどの用途として httptest.serve
フラグを渡すこともできます。-httptest.serve=127.0.0.1:18888
などとしてフラグを指定した場合、HTTP サーバはブロックされます。
x_test.go func TestX (t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client" ) })) defer ts.Close() res, err := http.Get(ts.URL) if err != nil { t.Error(err) } got, err := io.ReadAll(res.Body) res.Body.Close() if res.StatusCode != 200 { t.Errorf("GET %s: expected status code = %d; got %d" , ts.URL, 200 , res.StatusCode) } if string (got) != "Hello, client\n" { t.Errorf("expected body %v; got %v" , "Hello, client" , string (got)) } }
https://play.golang.org/p/i1Dz5alS4hQ
実際にはテストで用いる構造体のフィールドに URL を保持できるようにして httptest
で起動させたモックサーバの URL に切り替えるなどことが多いのではないか、と思います。
func setup () (client *Client, mux *http.ServeMux, serverURL string , teardown func () ) { mux = http.NewServeMux() apiHandler := http.NewServeMux() apiHandler.Handle(baseURLPath+"/" , http.StripPrefix(baseURLPath, mux)) server := httptest.NewServer(apiHandler) client = NewClient(nil ) url, _ := url.Parse(server.URL + baseURLPath + "/" ) client.BaseURL = url client.UploadURL = url return client, mux, server.URL, server.Close }
https://github.com/google/go-github/blob/6970a6306ef18c5692d43b9cb3e3841c6e5b8553/github/github_test.go#L37-L90
APIサーバのハンドラのテストをしたい 上記の「APIサーバにアクセスするテストをしたい」の項目では httptest.NewServer
でモックサーバを立てて、API クライアントという視点でリクエストを発行してテストをしました。今度は API サーバを提供する視点から、ハンドラの単体テストを実施する場合を考えてみます。httptest.NewRequest
を用いると、ハンドラの単体テストを簡潔に実施できます。以下のようなハンドラを考えてみます。
func helloHandler (w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte ("hello world!" )) }
このハンドラに対して GET リクエストしたときに hello world!
というレスポンスが得られるかどうかテストします。httptest.NewRequest
はテストのためのリクエストを生成するのに便利です。httptest.NewRequest
で生成した *http.Request
をハンドラに渡すことができます。また httptest.NewRecorder()
で生成できる *httptest.ResponseRecorder
を用いるとハンドラのレスポンスを記録できます。
これらを用いて、以下のようにハンドラから想定のレスポンスが得られるかどうかテストできます。
import ( "io/ioutil" "net/http/httptest" "testing" ) func TestHelloHandler (t *testing.T) { r := httptest.NewRequest("GET" , "/dummy" , nil ) w := httptest.NewRecorder() helloHandler(w, r) resp := w.Result() body, err := io.ReadAll(resp.Body) if err != nil { t.Errorf("cannot read test response: %v" , err) } if resp.StatusCode != 200 { t.Errorf("got = %d, want = 200" , resp.StatusCode) } if string (body) != "hello world!" { t.Errorf("got = %s, want = hello world!" , body) } }
$ go test -v === RUN TestHelloHandler --- PASS: TestHelloHandler (0.00s) PASS ok github.com/d-tsuji/go-sandbox/x 0.186s
httptest.NewRequest()
や httptest.NewRecorder()
を用いることで、Echo や go-swagger といった Web フレームワークを用いて構築しているハンドラも同様にテストできます。
ちなみにリクエストは http.NewRequestWithContext
(http.NewRequest
) でも生成できるのは? と思うかもしれません。何が異なるかというと、httptest.NewRequest
は http.Handler
に適しているリクエストを返却します。一方で http.NewRequestWithContext
(http.NewRequest
) は Client.Do
や Transport.RoundTrip
に適しているリクエストを返却します。そのため、単体テストの用途で直接ハンドラにリクエストを渡す場合は httptest.NewRequest
を用いることになります。
データ競合をテストで検知したい テストでレースコンディションを検知したい場合はどうすればよいでしょうか。レースコンディションが起きていないか確認するために -race
フラグが役に立ちます。
公式の The Go Blog の例を引用します。
race_test.go package mainimport ( "fmt" "math/rand" "testing" "time" ) func TestFoo (te *testing.T) { start := time.Now() var t *time.Timer t = time.AfterFunc(randomDuration(), func () { fmt.Println(time.Now().Sub(start)) t.Reset(randomDuration()) }) time.Sleep(5 * time.Second) } func randomDuration () time.Duration { return time.Duration(rand.Int63n(1e9 )) }
上記の実装は異なるゴルーチンから変数 t
の読み書きをしていて、レースコンディションを引き起こします。これは -race
をつけてテストすることで確認できます。
> go test -v -race race_test.go === RUN TestFoo 947.9969ms ================== WARNING: DATA RACE Read at 0x00c00009a030 by goroutine 9: command-line-arguments.TestFoo.func1() C:/Users/d-tsuji/go/src/github.com/d-tsuji/go-sandbox/race_test.go:15 +0x128 Previous write at 0x00c00009a030 by goroutine 8: command-line-arguments.TestFoo() C:/Users/d-tsuji/go/src/github.com/d-tsuji/go-sandbox/race_test.go:13 +0x194 testing.tRunner() C:/Go/src/testing/testing.go:909 +0x1a0 Goroutine 9 (running) created at: time.goFunc() C:/Go/src/time/sleep.go:168 +0x58 Goroutine 8 (running) created at: testing.(*T).Run() C:/Go/src/testing/testing.go:960 +0x658 testing.runTests.func1() C:/Go/src/testing/testing.go:1202 +0xad testing.tRunner() C:/Go/src/testing/testing.go:909 +0x1a0 testing.runTests() C:/Go/src/testing/testing.go:1200 +0x528 testing.(*M).Run() C:/Go/src/testing/testing.go:1117 +0x306 main.main() _testmain.go:44 +0x22a ================== 1.0319969s 1.6985715s 1.9336667s 2.22163s 2.7717493s 3.4048429s 3.7370048s 3.9208427s 4.4012115s --- FAIL: TestFoo (5.00s) testing.go:853: race detected during execution of test FAIL FAIL command-line-arguments 5.122s FAIL
上記のようにレースコンディションが発生している場合は、テストが FAIL になります。
-race
をつけてテストすると多少実行に時間がかかりますが、想定しないレースコンディションが発生して、テストが FAIL になるとデバッグが大変です。テストの際にデフォルトで -race
をつけておくのも1つの方法です。
テストデータを置いておきたい テストの Input や Output になるファイルを testdata
という名前のディレクトリに置いておくことができます。testdata
ディレクトリに含まれるファイルはテストのときのみ用いられます。標準パッケージの中でたくさん用いられていますが、image
パッケージの例を上げると src/image/testdata といったものです。
テストにヘルパー関数を使いたい 複数のテストで用いるような共通の関数をヘルパー関数として実装する場合があると思います。テスト用のヘルパー関数の特徴は以下です。
ヘルパー関数はエラーを返さない
*testing.T
を受け取ってテストを落とす
Go 1.9 からは T.Helper
を使って情報を補足する
Go Friday 傑作選 より引用
テスト用のヘルパー関数は以下のように実装されます。
func mustUrlParse (t *testing.T, s string ) *url.URL { t.Helper() u, err := url.Parse(s) if err != nil { t.Fatal(err) } return u }
ヘルパー関数に t.Helper()
を付与すると何が嬉しいのでしょうか。上記のヘルパー関数 mustUrlParse
を用いたテストを実施してみて確認してみます。
func TestX (t *testing.T) { type args struct { str string } tests := []struct { name string args args want int }{ {"normal1" , args{"http://example.com/" }, 0 }, {"normal2" , args{"%zzzzz" }, 1 }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { url := mustUrlParse(t, tt.args.str) }) } }
ヘルパー関数から t.Helper()
を除いた場合
go test -v === RUN TestX === RUN TestX/normal1 === RUN TestX/normal2 --- FAIL: TestX (0.00s) --- PASS: TestX/normal1 (0.00s) --- FAIL: TestX/normal2 (0.00s) x_test.go:44: parse %zzzzz: invalid URL escape "%zz" FAIL exit status 1FAIL github.com/d-tsuji/go-sandbox/x 0.299s
go test -v === RUN TestX === RUN TestX/normal1 === RUN TestX/normal2 --- FAIL: TestX (0.00s) --- PASS: TestX/normal1 (0.00s) --- FAIL: TestX/normal2 (0.00s) x_test.go:34: parse %zzzzz: invalid URL escape "%zz" FAIL exit status 1FAIL github.com/d-tsuji/go-sandbox/x 0.296s
上記の 2 通りの結果を比較すると FAIL になった行が何行目を指しているのか、が変わっています。
// t.Helper() がない場合 x_test.go:44: parse %zzzzz: invalid URL escape "%zz" // t.Helper() がある場合 x_test.go:34: parse %zzzzz: invalid URL escape "%zz"
t.Helper()
を付けた場合は t.Run()
内の _ = mustUrlParse(t, tt.args.str)
の行を指しています。付けなかった場合はヘルパー関数でエラーになった t.Fatal(err)
の行を指しています。t.Helper()
を付けた場合のほうが、テストケース内のどの行で失敗したか分かりやすくなり、エラーの原因を探りやすくなります。
ブラックボックステストをしたい あるパッケージ mypkg
があって以下のようなファイル構成になっているとします。ブラックボックステストなどを実施するときにパッケージ名を分けたい、などのニーズがあるかもしれません。Go ではディレクトリ名とパッケージ名は同じである必要があるため、異なるパッケージ名のファイルを作ることはできません。しかし例外的に mypkg_test
という、mypkg
に _test
を付与したパッケージ名は用いることができます。Test packages にもあるように Go では _test
の接尾語をもつパッケージ名は別のパッケージとして扱われます。パッケージ名を mypkg
と mypkg_test
とで分けることでテスト実施時に mypkg
の非公開の関数を呼び出すことはできなくなります。似たような議論を Proper package naming for testing with the Go language
で見ることができます。
具体的には以下のようなファイル構成になっていて x.go
と x_test.go
で別のパッケージ名にできます。ディレクトリは分けなくて良い、というのがポイントです。
x.go
x_test.go
用途(単体・インテグレーション)などによってテストを切り替えたい テストは単体テストの他、インテグレーションテストなど用途によっていくつか存在します。用途によって実行するテストを分けるにはどのようにすれば良いでしょうか。ビルドタグ を用いる方法やディレクトリを分ける方法があります。
以下のようにビルドタグ // +build integration
を付与します。このようにすると go test -tags=integration
とタグを付けたときのみテストが実行されるようになります。タグがない場合はテスト対象に含まれません。
package ximport ( "testing" ) func TestX (t *testing.T) { t.Log("run integration test" ) }
OSS から具体例をあげると以下のようなものです。以下の例はテストのパッケージ名に _test
をつけることでテストであることを明示的に分けています。テストで用いる関数は、テスト内でインポートしています。
package mtail_testimport ( "os" "path" "testing" "time" "github.com/google/mtail/internal/mtail" "github.com/google/mtail/internal/testutil" ) func TestNewProg (t *testing.T) { tmpDir, rmTmpDir := testutil.TestTempDir(t) defer rmTmpDir() logDir := path.Join(tmpDir, "logs" ) progDir := path.Join(tmpDir, "progs" ) err := os.Mkdir(logDir, 0700 ) if err != nil { t.Fatal(err) } err = os.Mkdir(progDir, 0700 ) if err != nil { t.Fatal(err) } m, stopM := mtail.TestStartServer(t, 0 , false , mtail.ProgramPath(progDir), mtail.LogPathPatterns(logDir+"/*" )) defer stopM() startProgLoadsTotal := mtail.TestGetMetric(t, m.Addr(), "prog_loads_total" ).(map [string ]interface {}) testutil.TestOpenFile(t, progDir+"/nocode.mtail" ) time.Sleep(time.Second) progLoadsTotal := mtail.TestGetMetric(t, m.Addr(), "prog_loads_total" ).(map [string ]interface {}) mtail.ExpectMetricDelta(t, progLoadsTotal["nocode.mtail" ], startProgLoadsTotal["nocode.mtail" ], 1 ) }
https://github.com/google/mtail/blob/master/internal/mtail/new_prog_integration_test.go#L1-L44
シンプルながら、インテグレーションテストは integration
などのディレクトリに対象のテストケースを含めることが多いのではないでしょうか。その integration
パッケージと // +build integration
のビルドタグを組み合わせる方法も考えられます。OSS だと以下のようなイメージです。
モックを使ってテストをしたい 外部サービスに依存する実装のテストをするときに、外部サービスをモックしたい場合があります。特に外部の API を実行した結果に依存する場合、API のレートリミットやテストの結果が外部 API の仕様変更などに引きづられ、テスト結果が不安定になります。どのようにすればモック化できるでしょうか。方法はいくつかありますが、基本的な考えはインタフェースを満たす実装を、本物の実装とモックで切り替えていくことになります。
データベースアクセスする処理をモックする簡単な疑似コードを見てみます。データベースからユーザの検索と作成をします。インタフェースのメソッドは以下のようにあるとします。
type DataStore interface { FindUser(ctx context.Context, id int ) (*User, error ) CreateUser(ctx context.Context, name string ) error }
type User struct { id int name string }
通常の実装は以下のような感じを想定します。ポイントは DataStore のインタフェースをファクトリ関数である NewDataStore
で返すところです。
type UserDataStore struct { *sql.DB } func NewDataStore (db *sql.DB) DataStore { return &UserDataStore{db} } func (u *UserDataStore) FindUser(ctx context.Context, id int ) (*User, error ) { var user *User row := u.QueryRowContext(context.Background(), `SELECT * FROM user WHERE userId = ?` , id) if err := row.Scan(&user); err != nil { return nil , err } return user, nil } func (u *UserDataStore) CreateUser(ctx context.Context, name string ) error { return nil }
あるテストをする際に、このデータベースに依存する実装をモックする例は以下のようになります。インタフェースを満たす、モックオブジェクトに想定の結果が取得できるメソッドを実施します。
type MockUserDataStore struct {}func NewMockDataStore () DataStore { return &MockUserDataStore{} } func (m *MockUserDataStore) FindUser(ctx context.Context, id int ) (*User, error ) { return &User{ id: 1 , name: "test user" , }, nil } func (m *MockUserDataStore) CreateUser(ctx context.Context, name string ) error { fmt.Println("create user. dummy" ) return nil }
このモックしたオブジェクトをテストで用いる場合は以下のようになります。
func TestXXX (t *testing.T) { storage := NewMockDataStore() user, err = storage.FindUser(context.TODO(), 1 ) }
上記のようにして外部依存する処理をモックできます。モックの struct やメソッドの実装が手間なので gomock
を使うこともあるかもしれません(gomock
に関して詳細は説明を割愛します)。
インタフェースを用いたテストに関しては Golangにおけるinterfaceをつかったテスト技法 が詳しいです。またインタフェースの埋め込みを用いたモックに関しては Golangにおけるinterfaceをつかったテストで Mock を書く技法 が詳しいです。
余談ですが、テストがモックに依存しすぎるとなんのためのテストをしているか分からなくなったり、リファクタリングのときに辛かったりするので、モックを使ったテストは用法用量に注意です。
コンテナを扱いたい モックの例は上記に記載したものの、コンテナなどを利用して実際のサービスに近い環境でテストしたい場合があるでしょう。コンテナを利用する際に docker compose up
などで単体テストに必要なサービスを起動させてテストする、という方法がありますが、この記事ではテストコードの中でコンテナを扱える Testcontainers を簡単に紹介します。
Testcontainers とはテストコード上で任意のコンテナを起動・停止できるドライバのようなライブラリです。Go に限らずJava, Python, Rustなど様々な言語をサポートしています。Testcontainers の利点の1つとして、外部プロセスに依存せず、テストコードで必要な依存関係を制御できることが挙げられます。
以下はコンテナとして Redis を利用したサンプルコードです。コードの詳細には立ち入りませんが、テストコードと実行結果からコンテナの起動と停止、削除している雰囲気がつかめると思います。
import ( "context" "testing" "github.com/stretchr/testify/require" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) func TestWithRedis (t *testing.T) { ctx := context.Background() req := testcontainers.ContainerRequest{ Image: "redis:latest" , ExposedPorts: []string {"6379/tcp" }, WaitingFor: wait.ForLog("Ready to accept connections" ), } redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true , }) testcontainers.CleanupContainer(t, redisC) require.NoError(t, err) }
実行ログから以下のようにコンテナの作成、起動、停止、削除がされていることが分かります。
$ go test -v === RUN TestWithRedis 2025/04/08 12:17:54 github.com/testcontainers/testcontainers-go - Connected to docker: Server Version: 25.0.3 API Version: 1.44 Operating System: Ubuntu 22.04.1 LTS Total Memory: 3921 MB Testcontainers for Go Version: v0.36.0 Resolved Docker Host: unix:///var/run/docker.sock Resolved Docker Socket Path: /var/run/docker.sock Test SessionID: fb7772e6a5be3636d28086c2598dcd8910987ceee02a676258c7c429831339d2 Test ProcessID: c20fd063-455f-4606-bc02-fed860b3b586 2025/04/08 12:17:54 🐳 Creating container for image redis:latest 2025/04/08 12:17:54 🐳 Creating container for image testcontainers/ryuk:0.11.0 2025/04/08 12:17:54 ✅ Container created: 8fd47942b7af 2025/04/08 12:17:54 🐳 Starting container: 8fd47942b7af 2025/04/08 12:17:55 ✅ Container started: 8fd47942b7af 2025/04/08 12:17:55 ⏳ Waiting for container id 8fd47942b7af image: testcontainers/ryuk:0.11.0. Waiting for: &{Port:8080/tcp timeout:<nil> PollInterval:100ms skipInternalCheck:false} 2025/04/08 12:17:55 🔔 Container is ready: 8fd47942b7af 2025/04/08 12:17:55 ✅ Container created: e384b63b50c5 2025/04/08 12:17:55 🐳 Starting container: e384b63b50c5 2025/04/08 12:17:55 ✅ Container started: e384b63b50c5 2025/04/08 12:17:55 ⏳ Waiting for container id e384b63b50c5 image: redis:latest. Waiting for: &{timeout:<nil> Log:Ready to accept connections IsRegexp:false Occurrence:1 PollInterval:100ms check:<nil> submatchCallback:<nil> re:<nil> log:[]} 2025/04/08 12:17:55 🔔 Container is ready: e384b63b50c5 2025/04/08 12:17:55 🐳 Stopping container: e384b63b50c5 2025/04/08 12:17:56 ✅ Container stopped: e384b63b50c5 2025/04/08 12:17:56 🐳 Terminating container: e384b63b50c5 2025/04/08 12:17:56 🚫 Container terminated: e384b63b50c5 --- PASS: TestWithRedis (1.91s) PASS ok sample 1.912s
詳しくは Testcontainersを用いてテスト実行前の docker compose up を無くし、Goで並列テストする の記事で解説しているので、合わせて参照ください。
ゴルーチンリークを検出したい ゴルーチンがリークするような実装になっていることをテスト時に確認したいかもしれません。公式のツールチェーンには含まれていませんが、goleak
はゴルーチンリークの検出に役に立ちます。このツールを用いるとテスト時にゴルーチンがリークしている場合にテストが FAIL になります。
簡単な例で確認してみます。
leak.go func X (send chan struct {}) { go func () { for { select { case <-send: return } } }() }
leak_test.go func TestDoLeak (t *testing.T) { closedchan := make (chan struct {}) close (closedchan) tests := []struct { name string ch chan struct {} }{ { name: "not leak" , ch: closedchan, }, { name: "leak" , ch: make (chan struct {}), }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { defer goleak.VerifyNone(t) X(tt.ch) }) } }
結果は以下のようになります。想定通り leak
のテストが FAIL になっていることが分かります。
> go test -v === RUN TestDoLeak === RUN TestDoLeak/not_leak === RUN TestDoLeak/leak --- FAIL: TestDoLeak (0.46s) --- PASS: TestDoLeak/not_leak (0.00s) --- FAIL: TestDoLeak/leak (0.46s) leaks.go:78: found unexpected goroutines: [Goroutine 11 in state chan receive, with github.com/d-tsuji/go-sandbox/t.X.func1 on top of the stack: goroutine 11 [chan receive]: github.com/d-tsuji/go-sandbox/t.X.func1(0xc000010360) C:/Users/d-tsuji/go/src/github.com/d-tsuji/go-sandbox/t/leak.go:13 +0x40 created by github.com/d-tsuji/go-sandbox/t.X C:/Users/d-tsuji/go/src/github.com/d-tsuji/go-sandbox/t/leak.go:10 +0x46 ] FAIL exit status 1FAIL github.com/d-tsuji/go-sandbox/t 0.761s
https://play.golang.org/p/wWGWe8_S0WN
まとめ テストに関する Go の基本的な機能をまとめてみました。なるべく使い方のイメージがわかるように具体的な例を多く載せました。参考にした素晴らしいドキュメントの URL は以下に記載しています。合わせてみてみてください。
参考資料