2020/08/15更新 : 「テストの失敗をレポートしたい」と「サブテストの一部のみ実施したい」の章を追加
はじめに TIG の辻です。今回は春の入門祭り ということで Go のテストに入門してみよう! という記事です。
書いた背景ですが Go の標準ライブラリのコードリーディング会で testing
パッケージにチャレンジしてみましたが、難しすぎてわからん。そもそも Go のテストって何ができるんだっけ? という話になり、基本的な内容をなるべく具体例をまじえながらまとめました。
ざっとどんなことができるんだろう、という index になれば幸いです。
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 -v === RUN TestAdd === RUN TestAdd/normal --- PASS: TestAdd (0.00s) --- PASS: TestAdd/normal (0.00s) PASS
上記のテストは TableDrivenTest とサブテストを組み合わせています。どちらも現場でよく使われます。サブテストを用いると各テストごとに結果がわかるようになります。 TableDrivenTest はさまざまな Input/Output パターンを網羅するのに便利です。上記のテストをシンプルに書き直すと以下のようになります。
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 test -v === RUN TestAdd --- PASS: TestAdd (0.00s) PASS
テストの失敗をレポートしたい Go はテストのアサーションを提供していません。理由は公式のFAQ で紹介されています。先程の TestAddc
関数のように、テストが失敗したことを開発者が自ら実装する必要があります。失敗したことを示すには T.Error (T.Errorf)
や T.Fatal (T.Fatalf)
を用いることができます。
T.Fatal
を用いると T.Fatal
が実行された以降のテストは呼び出されずに終了します。テストが失敗したことを示すには T.Error
を使い、テストの初期化など、処理が失敗するとその後のテストが無意味になる場合は T.Fatal
を用いると良いでしょう。以下のように t.Fatalf
を用いた場合は、その後のテストの処理 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() ..." ) }) } }
$ go test -v === RUN TestAdd === RUN TestAdd/fail diff_test.go:31: add() = 3, want 30 panic.go:617: defer! diff_test.go:24: cleanup! === RUN TestAdd/normal diff_test.go:34: after add() ... diff_test.go:35: defer! diff_test.go:24: cleanup! --- FAIL: TestAdd (0.00s) --- FAIL: TestAdd/fail (0.00s) --- PASS: TestAdd/normal (0.00s) FAIL exit status 1 FAIL github.com/d-tsuji/go-sandbox 0.156s
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 }
このテストをスキップしたいとします。その場合は func (c *T) Skip(args ...interface{})
というメソッドを用いることでスキップできます。
テストコードに以下を追加します。
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
というフラグを付与したときだけテストがスキップされる、そうでないときはスキップされずテストが実施される、というように使い分けることができます。
標準パッケージでもテストのスキップが実装されているのを色々見ることができます。以下は 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
並列にテストを実施するには func (t *T) Parallel()
メソッドを用いることができます。上記のテストに tt := tt
と t.Parallel()
を追記します。
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) } }) }
上記の実装を用いてテストをします。するとテストが並列に実施されていることが分かります。結果としてテストにかかった時間は約 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
ループ時に割り当てているローカル変数 tt
を捕捉することは重要です。Go ではループで用いられる変数は同じアドレスを使います。
func main () { b := []byte ("abcde" ) for i, c := range b { fmt.Printf("i: %#v, c: %#v ------- &i: %#v, &c: %#v\n" , i, string (c), &i, &c) } }
// 変数のアドレスがすべて同じアドレスを参照している i: 0, c: "a" ------- &i: (*int)(0x40e020), &c: (*uint8)(0x40e024) i: 1, c: "b" ------- &i: (*int)(0x40e020), &c: (*uint8)(0x40e024) i: 2, c: "c" ------- &i: (*int)(0x40e020), &c: (*uint8)(0x40e024) i: 3, c: "d" ------- &i: (*int)(0x40e020), &c: (*uint8)(0x40e024) i: 4, c: "e" ------- &i: (*int)(0x40e020), &c: (*uint8)(0x40e024)
https://play.golang.org/p/_tSALjq8yZ6
そのため、テストを並列に実行するときは tt := tt
などとして変数をシャドウイングし、並列で実行しているテストに影響がないようにする必要があります。
ただし、テストケース自体がそもそも並列に実行できない場合、例えばデータベース上のテーブルへの UPDATE や INSERT が発生し、テストケースで競合する場合、テストを並列に実行することはできないため、注意が必要です。
テストの前処理や後処理を実施したい テストをしていると、前処理や後処理をしたい場合があると思います。主な例の 1 つとしてデータベースの処理化があるでしょう。テストを実施する前処理としてあるデータを INSERT しておいて、テスト実施後に対象のテーブルのデータを削除する、といったものです。
そのような共通的な前処理や後処理を実施したい場合は
func TestMain (m *testing.M)
関数を用いることができます。
例を見てみます。
func f () { fmt.Println("なんらかの処理" ) }
以下のように TestMain
を用いてテストを制御するとテストの前後(今回の場合は f()
の前後)に処理を実行できます。m.Run()
の実行結果を取得して os.Exit()
するのが慣用的です。
func Test_f (t *testing.T) { f() } func TestMain (m *testing.M) { fmt.Println("前処理" ) status := m.Run() fmt.Println("後処理" ) os.Exit(status) }
$ go test 前処理 なんらかの処理 PASS 後処理 ok github.com/d-tsuji/go-sandbox 0.298s
参考までに google/trillian
という OSS で TestMain
がどのように使われているか確認してみます。以下はテスト用のデータベースを openTestDBOrDie()
として作成し、テスト終了後に done(context.Background())
という処理を実施しています。
func TestMain (m *testing.M) { flag.Parse() if !testdb.PGAvailable() { glog.Errorf("PG not available, skipping all PG storage tests" ) return } var done func (context.Context) db, done = openTestDBOrDie() status := m.Run() done(context.Background()) os.Exit(status) }
https://github.com/google/trillian/blob/master/storage/postgres/storage_test.go#L168-L181
Go1.14 でテスト時に生成したリソースを便利に後処理できる関数が登場しました。T.Cleanup です。
func (c *T) Cleanup(f func () )
リソースの後処理という意味では defer
を用いてクリーンアップできるので、今までとあまり変わらないのでは? と思うかもしれません。T.Cleanup
を便利に使えることが実感できるシーンの 1 つとして、テストに必要な前処理をテストとは別の関数で実施している場合があります。
簡単な例ですが、テストの前準備としてテスト用のファイルを生成する必要があったとして、テスト終了後に削除したい場合、以下のような実装が考えられます。TempFile 関数ではリソースをクローズする処理を呼び出し元に返却する必要があり、呼び出し元で後処理として teardown 関数を呼び出すことになります。
testutil/file.go func TempFile (t *testing.T, content []byte ) (name string , teardown func () ) { file, err := ioutil.TempFile("" , "test" ) if err != nil { t.Error(err) } if err = ioutil.WriteFile(file.Name(), content, 0644 ); err != nil { t.Error(err) } return file.Name(), func () { syscall.Unlink(file.Name()) } }
呼び出し元の処理です。
file, delete := testutil.TempFile(t, nil ) defer delete ()
T.Cleanup
を用いると前処理を実施する関数内でリソースの後処理が実施できるようになります。TempFile 関数の例であれば、以下のように t.Cleanup
を用いることができます。関数から return したタイミングで呼び出される defer
とは異なり、テストが完了したタイミングで Cleanup 処理が呼び出されます。
func TempFile (t *testing.T, content []byte ) string { file, err := ioutil.TempFile("" , "test" ) if err != nil { t.Error(err) } t.Cleanup(func () { syscall.Unlink(file.Name()) }) if err = ioutil.WriteFile(file.Name(), content, 0644 ); err != nil { t.Error(err) } return file.Name() }
ファイルでなくても、テスト用のデータベースや 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
このような感じでどのパスが通っていないか確認できます。
あるディレクトリ配下のテストをすべて実施したい たとえば標準パッケージの例だと、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
フラグを用いることができます。
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) } }) } }
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
を用いてもビルドキャッシュ全体を削除でき、テストのキャッシュも削除できます。
テストコードの雛形を楽に作りたい Tips ですが 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/master/src/net/http/request_test.go#L210-L219
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
の結果は何が同値で、何が同値でなかったか、同値でなかったときは取得した値と想定する値は何か明示的にわかるのがよいです。他にもオプションで条件をカスタマイズできます。(やりすぎ注意)
社内でも stretchr/testify/assert
を使う勢なども見かけます。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 := ioutil.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/master/github/github_test.go#L31-L64
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 := ioutil.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
をつけることで確認できます。-race
は go test
以外にも以下のように使うことができます。
go test -race mypkg // test the package go run -race mysrc.go // compile and run the program go build -race mycmd // build the command go install -race mypkg // install the package
> 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
パッケージの例を上げると https://github.com/golang/go/tree/master/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 を書く技法 が詳しいです。
余談ですが、テストがモックに依存しすぎるとなんのためのテストをしているか分からなくなったり、リファクタリングのときに辛かったりするので、モックを使ったテストは用法用量に注意です。
ゴルーチンリークを検出したい ゴルーチンがリークするような実装になっていることをテスト時に確認したいかもしれません。公式のツールチェーンには含まれていませんが、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 は以下に記載しています。合わせてみてみてください。
参考資料