2018年夏(Go1.10)時点でGoのテスト方法をまとめる。
この記事は以下のスライド資料の補足記事になる。
- Tour of testing in 2018
- https://speakerdeck.com/budougumi0617/tour-of-testing-in-2018
TL;DR
- Goでテストを行なうときの方法をまとめた。
- 原則標準パッケージ・標準コマンドの説明のみ
- サンプルリポジトリは以下
testing パッケージ
https://golang.org/pkg/testing/
まずは基本としてtestingパッケージがある。Goのテストで使うメソッドは基本的にこのpkg配下にある。(HTTP関連のテストで使うhttptestはnet/httppkg配下にある。)
Goでテストを書くときはtestingパッケージを使う。
テストはxxx_test.goというファイルで作成する。
この命名規則を守ることでgo buildのときは無視され、go testのときだけビルドされる。
テストメソッドは*testing.T型を引数として、TestXxxという命名規則で書かれる。Testのあとの単語もキャメルケースで命名する必要がある。TestSumは実行されるが、Testsumは実行されない。
func TestSum(t *testing.T) {
a, b, want := 1, 2, 3
if got := Sum(a, b); got != want {
t.Fatalf("want = %d, got = %d", want, got)
}
}
構造体メソッドのテストを書くときはTestSturct_Methodという命名するのが望ましい(Examplesを書くときとフォーマットを合わせるため)。
テストデータを用意する
テスト用のデータはtestdataディレクトリに配置する。
JSONやバイナリデータなどをテスト結果として用意するときは.golden拡張子でtestataディレクトリに入れておく。
- https://golang.org/cmd/go/#hdr-Test_packages
- Advanced Testing with Go
- Testing with golden files in Go
テストを実行する
テストの書き方の前にテストの実行方法をまとめる。テストはgo testコマンドで実行する。
パッケージを指定して実行する
go testコマンドは以下のように実行できる。
# カレントパッケージのテストを実行する
$ go test
# パスで指定されたパッケージのテストを実行する(相対パス指定が可能)
$ go test ./table
# サブパッケージを含めたパスで指定されたパッケージ以下のテストを全て実行する
$ go test ./...
Go1.10から事前にgo vetコマンドが実行されている
Go1.10からはgo testが実行される前にgo vetが実行される。
どうしてもgo vetを通さずテストの結果をみたい場合はgo test -vet=offとする。
-
CL74356 cmd/go: run vet automatically during go test
-
Go 1.10 ツール周辺の CL を読む
Go1.10からテスト結果がキャッシュされている
Go1.10からgo buildの結果がキャッシュされるように、go testの結果もキャッシュされる(つまり実行されない。特定のオプション時は除外)。
キャッシュを削除したいときはgo clean -testcacheコマンドでキャッシュを削除する。
キャッシュを使わずにテストを実行するだけならば-count=1オプションをつけてgo testを実行する。
# t/parallel パッケージの結果だけキャッシュされている
$ go test ./...
ok github.com/budougumi0617/go-testing/t 0.009s
ok github.com/budougumi0617/go-testing/t/parallel (cached)
ok github.com/budougumi0617/go-testing/t/table 0.009s
# 全ての結果がキャッシュされている(つまり何も実行していない)
$ go test ./...
ok github.com/budougumi0617/go-testing/t (cached)
ok github.com/budougumi0617/go-testing/t/parallel (cached)
ok github.com/budougumi0617/go-testing/t/table (cached)
# キャッシュを使わずに全て実行する
$ go test ./... -count=1
ok github.com/budougumi0617/go-testing/t 0.011s
ok github.com/budougumi0617/go-testing/t/parallel 0.011s
ok github.com/budougumi0617/go-testing/t/table 0.010s
-
How Go cache tests
-
Go 1.10 Release Notes
特定のテストファイルだけを実行する
同じパッケージのメソッドを対象にした特定のテストファイルを明示的に指定してテストする場合は、テスト対象のファイルも引数に指定する。
# sum.goの中にテストするメソッドが含まれている
$ go test sum_test.go sum.go
後述するpackage XXX_testを使ったテストの外部パッケージ化をしているときは必然的にimportしているのでテストファイルだけで実行できる。
特定のテストケースだけを実行する
特定のテストケースだけ実行する場合は-runオプションで指定することができる(正規表現可)。
# カレントパッケージ配下にある"Minus"というサブテスト(後述)だけを実行するとき
$ go test ./... -v -run /Minus
-parallel nオプションで最大並行実行数を制御する
後述するT.Parallel()メソッドが呼ばれているテストケースは並行に実行される。
同時に実行されるテストケースの数はデフォルトだとGOMAXPROCSの数と等しい。
-parallel nオプションを使うと、nの数で同時実行数を制御できる。
-cpu listオプションでGOMAXPROCSを変更しながらテストする
-cpuオプションは実行時のGOMAXPROCSの数を変更することができる。
オプション引数には数字の羅列を渡すことができ、例えば-cpu 1,2,4とするとGOMAXPROCSを変えながら都合3回テストが実行される。
- go testの並列(-cpuと-parallel)がなんの事だったか忘れた時のメモ #golang
デバッガ(delve)でテストを実行する
Delveでテストのデバッグ実行をするときは以下のようにオプションを渡す。
普段-runと指定していても、正式なオプションは-test.runなどだったりする。
$ dlv test -- -test.run TestCreateTempFile
CI上でテストを実行する
JUnit形式で結果を出力するとCIの機能でテスト結果を可視化してくれたりする。
CircleCIの場合は公式にやり方が書いてある。
- run:
name: Run unit tests
command: |
trap "go-junit-report <${TEST_RESULTS}/go-test.out > ${TEST_RESULTS}/go-test-report.xml" EXIT
go test -v ./... | tee ${TEST_RESULTS}/go-test.out
サンプルリポジトリでCircleCIを実行した結果はこちら
-
Language Guide: Go
playgroundでテストを実行する
ちょっと前からPlaygroundでもテストメソッドを簡単に実行できるようになっている。
main()メソッドを宣言せずにコードを書けばよい。
https://play.golang.org/p/Timl696Usih
- The Go Playground で Test & Example がしやすくなった
テストの書き方
テストコードのパッケージ名について
Goは同じディレクトリ以下に複数のpackage名を許さないが、例外的にxxxパッケージとxxx_testパッケージは同じディレクトリに共存できる。
当然異なるパッケージになるので、非公開メソッドなどは参照できなくなるが、循環参照を回避できる。
また、非公開な機能をexport_test.goを介してアクセスすることもできる。
- Go Fridayこぼれ話:非公開(unexported)な機能を使ったテスト #golang
testing.TBインターフェースの主なメソッド
通常のテスト用のtesting.Tとベンチマーク用のtesting.BはErrorfなどいくつか共通メソッドを持っている。
それらはtesting.TBインターフェースのメソッドとして定義されている。Goのテストはここで定義されているメソッドを使ってテストの正否を判定する。
TB.Error/TB.Errorfはテストの失敗が記録されるが、後続処理は継続されるTB.Fatal/TB.Fatalfはテストの失敗が記録されテストケースは終了する。その場で宣言済みのdefer処理なども開始されるTB.Failはテストが失敗したことが記録されるが、その後の処理は継続される。TB.Errorなどが内部的に呼んでいるTB.FailNowはその場でテストケースが終了し、defer処理などが開始される。TB.Fatalなどが内部的に呼んでいるTB.SkipNow/TB.Skipはテストがその場でスキップされる。テストケースを一時的に無効にしたいときに使う。TB.Log/TB.Logfは-vオプションがついていたとき、テストがFailしたときに出力される。ベンチマーク時は常に出力される。
testing.TB.Helperメソッド
テストケースを複数書いていると、共通処理を外出ししてサブメソッドにすることがしばしばある。
(たとえば構造体を走査しててエラーを判断したり)
そうすると、エラーログがサブメソッド内になってしまう。Go1.9からそれを防ぐTB.Helperメソッドが追加された。
サブメソッド内で同メソッドを呼ぶと、サブメソッド内でFailしても呼び出し元メソッドの情報が出力される。
何もしないサブメソッドと、TB.Helper()メソッドの呼び出すサブメソッドをhelper_test.goファイルに記述し、
package helper_test
import (
"testing"
)
func errorf(tb testing.TB, want, got int) {
tb.Errorf("want = %d, got = %d", want, got)
}
func errorfHelper(tb testing.TB, want, got int) {
tb.Helper()
tb.Errorf("want = %d, got = %d", want, got)
}
それぞれをsample_test.goファイル内のテストケースから呼び出すと、
package helper_test
import (
"testing"
)
// Sum returns two int values.
func Sum(a, b int) int {
return a + b
}
func TestSum(t *testing.T) {
a, b, want := 1, 2, 4
if got := Sum(a, b); got != want {
errorf(t, want, got)
errorfHelper(t, want, got)
}
}
TB.Helper()メソッドを呼んだほうだけ呼び出し元のsample_test.goの情報を持ってエラーが出力される。
$ go test
--- FAIL: TestSum (0.00s)
helper_test.go:8: want = 4, got = 3
simple_test.go:16: want = 4, got = 3
FAIL
exit status 1
FAIL github.com/budougumi0617/go-testing/t/helper 0.010s
Table Driven Test
Goではテーブルを使ってテストを書くことが推奨されている。
- テーブルでテストを用意することで、テーブル内容を確認すれば評価済みの入出力セットをすぐ確認することができて見通しが良い。
- また、バグが発見されたときはテーブルに「バグ発生時の入力とあるべき出力」を追加するだけでTDDがスタートできる。
func TestSum(t *testing.T) {
tests := []struct {
a, b, want int
}{
{0, 1, 1},
{-1, -1, -2},
{-3, 2, -1},
}
for _, tt := range tests {
if got := parallel.Sum(tt.a, tt.b); got != tt.want {
t.Fatalf("want = %d, got = %d", tt.want, got)
}
}
}
for _, tt := range tests
標準パッケージで確認すると、テーブル駆動テストをするときはtestsで宣言してループをttで流していることが多いので、それに従って書くことが多い。(for _, test := range testsも多いが、testは長いのでttを選ぶ。)
Sub Test(testing.T.Run)
testing.T.Runメソッドを使うとサブテスト書くことができる。
func TestSum(t *testing.T) {
tests := []struct {
name string
a, b, want int
}{
{"Simple", 0, 1, 1},
{"Minus", -1, -1, -2},
{"Both", -3, 2, -1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parallel.Sum(tt.a, tt.b); got != tt.want {
t.Fatalf("want = %d, got = %d", tt.want, got)
}
})
}
}
テーブルテストとサブテストを組み合わせることで以下の効果が得られる。
- テーブルのテストケースそれぞれでテストの可否を判断することができる
- 後述する
testing.T.Parallelメソッドを使うことで並行実行ができる -test.runオプションでサブテストそれぞれを個別に実行することができる
サブテストの名前はtesting.T.Runの第一引数に渡した文字列になる。スペースを含んだ文字列は_で置換されるので、使わないほうがよい。
「このテストケースでは何をテストしている(テストしたい)のか?」がわかりやすいようにtable要素で名前をつけたほうがテストコードの可読性が高い。
tests := []struct {
name string
// ...
}{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// ...
})
}
冒頭のTestSumを実行すると以下のようになる。
$ go test ./... -v
=== RUN TestSum
=== RUN TestSum/Simple
=== PAUSE TestSum/Simple
=== RUN TestSum/Minus
=== PAUSE TestSum/Minus
=== RUN TestSum/Both
=== PAUSE TestSum/Both
=== CONT TestSum/Simple
=== CONT TestSum/Both
=== CONT TestSum/Minus
--- PASS: TestSum (0.00s)
--- PASS: TestSum/Simple (0.00s)
--- PASS: TestSum/Both (0.00s)
--- PASS: TestSum/Minus (0.00s)
PASS
ok github.com/budougumi0617/go-testing/t/parallel 0.009s
-runオプションで特定のサブテストだけ実行する例は以下になる。
$ go test ./... -v -run Sum/Both
=== RUN TestSum
=== RUN TestSum/Both
=== PAUSE TestSum/Both
=== CONT TestSum/Both
--- PASS: TestSum (0.00s)
--- PASS: TestSum/Both (0.00s)
PASS
ok github.com/budougumi0617/go-testing/t/parallel 0.011s
wantとgot
expectやactualは単語が長いので、wantとgotで書くほうがベターに思える。
- Go testing style guide
エラーは無視しない
ある一つのメソッドのテストを一つのテーブルに書いていると、以下のようなテストも同じテーブルに入れたくなる。
- 通常ケースのテスト(エラーが出ないテスト)
- ある特定のエラーがでたことを確認するテスト
その場合はテストケース内でエラーの有無をフラグとして検証条件を増やす。
tests := []struct {
name string
in int
want int
wantError bool
err error
}{
{"Basic", 4, 4, false, nil},
{"HasError", -1, 0, true, errors.New("Negative value")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pt := we.PositiveInt(tt.in)
got, err := pt.Value()
if !tt.wantError && err != nil {
t.Fatalf("want no err, but has error %#v", err)
}
if tt.wantError && !reflect.DeepEqual(err, tt.err) {
t.Fatalf("want %#v, but %#v", tt.err, err)
}
if !tt.wantError && got != tt.want {
t.Fatalf("want %q, but %q", tt.want, got)
}
})
}
エラーの有無だけではなく期待するエラーかも確認するのが望ましい。
Errorfのテンプレート文字列
文字列を出力するときは%#vを使ったほうが良い(ときもある)。微妙に空白が混ざっていたとしても可視化できる。
package main
import (
"fmt"
)
func main() {
spaces := " \n "
fmt.Printf("spaces = %v\n", spaces) // spaces =
fmt.Printf("spaces = %#v\n", spaces) // spaces = " \n "
}
testing.T.Parallelメソッドで並行実行するテストを書く
Goのテストは通常逐次的に実行される。並行実行オプションをつけて実行してもそれぞれのテストケースは逐次的にされる。
テストケースの中でT.Parallel()メソッドが呼ばれているテストケースのみが並行に実行される。
各サブテストを並行実行したい場合は、testing.T.Run()メソッドで呼ぶfunc(*testign.T)メソッド内で呼ぶ。
for _, tt := range tests {
tt := tt // Don't forget when parallel test
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := Sum(tt.a, tt.b); got != tt.want {
t.Fatalf("want = %d, got = %d", tt.want, got)
}
})
}
ループ変数のttをローカル変数で補足するのを忘れないこと。
- 初級者向けGoの落とし穴と解説 - ループ変数の補足
TestMain(m *testing.M)メソッドで前処理を定義する
パッケージのテストを実行する前後で任意の処理を実行してからテストを行いたいときはTestMain(m *testing.M)を宣言しておく。
m.Run()前後に処理を記述することで、そのパッケージのテストの実行前後に処理を割り込ませることができる。
func TestMain(m *testing.M) {
func() {
fmt.Println("Prepare test")
}()
ret := m.Run()
func() {
fmt.Println("Teardown test")
}()
os.Exit(ret)
}
$ go test
Prepare test
PASS
Teardown test
ok github.com/budougumi0617/go-testing/t/testmain 0.009s
当然(?)だが、特定のテストファイルのみを引数に実行した場合はTestMainは実行は実行されない。
# TestMainはmain_test.goに含まれている
$ go test sumsub_test.go -v
=== RUN TestSum
...
--- PASS: TestSub/Both (0.00s)
PASS
ok command-line-arguments 0.011s
# TestMainを含んだmain_test.goと一緒に実行する
go test sumsub_test.go main_test.go -v
Prepare test
=== RUN TestSum
...
--- PASS: TestSub/Both (0.00s)
PASS
Teardown test
ok command-line-arguments 0.009s
- Goでテストを書く(テストの実装パターン集) - パッケージ全体でSetup/Teardownを使う
ビルドタグでテストケースを分類する
go buildと同じように、go testもビルドタグを使うことができる。
テストファイルごとに適切なビルドタグを指定しておけば、ビルドタグが有効なときだけ実行されるテストケースを宣言しておくことができる。
-tags integrationオプションが付与されたときだけ有効なテストファイルを作成し、
// +build integration
package tags_test
import (
"testing"
"github.com/budougumi0617/go-testing/t/tags"
)
func TestSub(t *testing.T) {
// some testing...
}
通常のファイルは-tags integrationオプション有効時に無効にしておけば、
// +build !integration
package tags_test
import (
"testing"
"github.com/budougumi0617/go-testing/t/tags"
)
func TestSum(t *testing.T) {
// some testing...
}
複数目的のテストを分類して実行できる。
$ go test -v
=== RUN TestSum
--- PASS: TestSum (0.00s)
PASS
ok github.com/budougumi0617/go-testing/t/tags 0.009s
$ go test -v -tags integration
=== RUN TestSub
--- PASS: TestSub (0.00s)
PASS
ok github.com/budougumi0617/go-testing/t/tags 0.009s
テストカバレッジを集計する
Goは標準機能でテストカバレッジを計測することができる。
Go1.10からは複数パッケージのカバレッジも簡単に取得できるようになった。
単純にコマンドライン上に計測結果を出力したいならばgo testコマンドに-coverオプションをつけて実行すればよい。
$ go test -cover ./...
HTMLにカバレッジを保存する
go toolと組み合わせるとカバレッジの計測結果を
-covermode=count(atomic)オプションをつけておくと各コード行の実行回数も計測できる。
$ go test ./... -covermode=count -coverprofile=c.out
$ go tool cover -html=c.out -o coverage.html
CIと組み合わせる
たとえばCircle CIならばCI中の成果物を保存しておく設定があるので、これで前述のHTMLファイルを保存しておく。
- store_artifacts:
path: /code/test-results
destination: prefix
Codecovと連携するならばCI中で以下のようにスクリプトを呼ぶだけで結果を送信できる。
$ go test ./... -coverprofile=coverage.txt -covermode=count
$ bash <(curl -s https://codecov.io/bash)
サンプルリポジトリを実行したCodecovの結果はこちら。
-
Configuring CircleCI
-
Codecov Go Example
2019/10追記
この記事をまとめた際はGo1.10だった。Go1.11からGo1.13で追加・変更されたGoの仕様を以下にまとめた。
関連
- Goのtestingを理解する in 2018 - Examples編 #go
- Goのtestingを理解する in 2018 - quickサブパッケージ編 #go
- Goのtestingを理解する in 2018 - iotestサブパッケージ編 #go
TODO ベンチマーク、サブパッケージなどを別記事にまとめる。