テックタッチアドベントカレンダー11日目を担当する @taisa831 です。10日目は @mxxxxkxxxx の「Go言語 ElastiCacheの その前に」でした。575 の 5 が Go に掛かっていていい感じですね!もちろん内容も良い!
本記事では、Go
のtesting
パッケージについて書きます。既存記事を調べてみると、そこそこあるけどそこまで多くはない。重厚な記事もあればあっさりした記事もある。ということで深すぎず浅すぎずを目指そうと思います。
「testing パッケージの基本を理解する」なのでGo
の testing パッケージ を参考にしました。テストに関しては最初アサーションがないことに戸惑いましたが、慣れたらない方がよく感じてきました。執筆時点でのバージョンはgo1.13.4
です。
- もっともシンプルなテスト
- Benchmark を取る
- Example テスト
- カバレッジをとる
- テストをスキップする
- サブテストとサブベンチマーク
- 共通処理のエラー箇所を分かりやすく出力する
- 前処理/後処理をする
- その他
- おわりに
もっともシンプルなテスト
Go
でテストを実行するにはいくつかルールがあります。
- ファイル名は
**_test.go
とする - 関数は
TestCamelCase
のようにTest
ではじめ後ろはCamelCase
とする -
testing
パッケージを引数で受ける - テストファイルはテスト対象ファイルと同一パッケージとする(※テストだけ例外的に
packagename_test
とすることも可能)
そして下記のように期待値と実行時の値が違う場合はt.Errorf
でエラーを記録します(後述しますが必ずしもt.Errorf
である必要はありません)。
func TestAbs(t *testing.T) {
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
}
$ go test
ok command-line-arguments 4.018s
-v
オプションをつけることでより詳細な情報が見られます。
go test -v
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok command-line-arguments 0.005s
パッケージ/ファイル単位でテスト対象を指定する
パッケージ単位でテスト対象を指定するには以下のようにコマンドを使い分けます。
# カレントディレクトリ
$ go test
# カレント配下全て
$ go test ./...
# testing パッケージ配下
$ go test testing
# testing/quick パッケージ配下
$ go test testing/quick
# testing 配下全て
$ go test testing/...
# 同じパッケージがテストが対象の場合
$ go test testing_testing.go testing.go
# packgename_test のようにパッケージを分けている場合
$ go test testing_testing.go
Benchmark
を取る
testing
パッケージを使ってBenchmark
を取ることもできます。Benchmark
を取るには*testing.B
を使い、-bench
オプションを指定して実行します。
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}
以下の実行時のベンチマークは21,764,674 times at a speed of 54.1 ns per loop
となります。
$ go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/taisa831/sandbox-go/testing
BenchmarkHello-8 21764674 54.1 ns/op
PASS
ok github.com/taisa831/sandbox-go/testing 1.241s
一部の処理だけBenchmark
を取る
Benchmark
にはStartTimer()
、StopTime()
、ResetTimer()
が用意されています。これらを利用して一部の処理だけピンポイントでベンチマークをとることもできます。
func BenchmarkBig(b *testing.B) {
// 重い処理
Big()
// リセット
b.ResetTimer()
// ここから計測が始まる
for i := 0; i < b.N; i++ {
fmt.Sprintf("hello")
}
}
重い処理は無視して先ほどと同じような結果となるベンチマークが取れました。
go test -bench=.
goos: darwin
goarch: amd64
pkg: github.com/taisa831/sandbox-go/testing
BenchmarkBig-8 22457809 54.2 ns/op
PASS
ok github.com/taisa831/sandbox-go/testing 14.274s
Example
テスト
testing
パッケージには少し特殊なものとして出力をテストするExample
テスト機能があります。Example
テストは**_test.go
内で実行することができます。出力をチェックするにはOutput:
やUnordered output:
を利用します。
Output:
Output: hello
と書くことでテストができます。例えばOutput: helle
に変えるとテストでエラーとなります。
func ExampleHello() {
fmt.Println("hello")
// Output: hello
}
go test -v
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
PASS
ok github.com/taisa831/sandbox-go/testing 0.005s
複数行出力した結果のテストも可能です。
func ExampleSalutations() {
fmt.Println("hello, and")
fmt.Println("goodbye")
// Output:
// hello, and
// goodbye
}
go test -v
=== RUN ExampleSalutations
--- PASS: ExampleSalutations (0.00s)
PASS
ok github.com/taisa831/sandbox-go/testing 0.005s
Unorderd output:
Unordered output
はその名の通りオーダーを無視して出力を検証してくれます。
func ExamplePerm() {
for _, value := range rand.Perm(5) {
fmt.Println(value)
}
// Unordered output: 4
// 2
// 1
// 3
// 0
}
go test -v
=== RUN ExamplePerm
--- PASS: ExamplePerm (0.00s)
PASS
ok github.com/taisa831/sandbox-go/testing 0.005s
Example
テストはGoDoc
にも利用でき、下記のような命名規則があります。
- func Example() { ... }
-
GoDoc
のPackage
に出る
-
- func ExampleF() { ... }
-
GoDoc
のFunction
に出る
-
- func ExampleT() { ... }
-
GoDoc
のType
に出る
-
- func ExampleT_M() { ... }
-
GoDoc
のType
のMethod
に出る
-
更に上記の単位で細かくExample
テストを分けて書きたい場合は_suffix
のように小文字で記述します。
- func Example_suffix() { ... }
- func ExampleF_suffix() { ... }
- func ExampleT_suffix() { ... }
- func ExampleT_M_suffix() { ... }
GoDoc
にExample
を出力してみる
実際にどのようにGoDoc
に出るかイメージしにくいのでサンプルを作成して確認してみます。ここまではテストファイルしかなかったので、別途サンプル実装とテストを書いて実行してみました。
// Package person
package person
type Person struct {
firstName string
lastName string
}
// NewPerson
func NewPerson(firstName string, LastName string) *Person {
return &Person{firstName: firstName, lastName: LastName}
}
// GetFirstName
func (p *Person) GetFirstName() string {
return p.firstName
}
// GetLastName
func (p *Person) GetLastName() string {
return p.lastName
}
func Example() {
person := NewPerson("Taro", "Yamada")
fmt.Println(person.GetFirstName())
// output: Taro
}
func ExampleNewPerson() {
person := NewPerson("Taro", "Yamada")
fmt.Println(person.GetFirstName())
// output: Taro
}
func ExamplePerson_GetFirstName() {
person := NewPerson("Taro", "Yamada")
fmt.Println(person.GetFirstName())
// output: Taro
}
func ExamplePerson_GetLastName() {
person := NewPerson("Taro", "Yamada")
fmt.Println(person.GetLastName())
// output: Yamada
}
上記サンプルコードをGoDoc
に出力してみます。GoDoc
が入っていない場合はgo get
してからコマンドを実行しlocalhost:6060
にアクセスします。Packages
にアクセスすると自分のパッケージ情報が見られます。
$ go get golang.org/x/tools/cmd/godoc
$ godoc -http=:6060
# localhost:6060にアクセス
Example()
はPackage
に表示される
ExampleNewPerson()
はType
に表示される
ExamplePerson_GetFirstName()
とExamplePerson_GetLastName
はfunc
に表示される
カバレッジをとる
上記サンプルにテストを追加しカバレッジをとってみます。カバレッジは-cover
オプションで簡単にみることができます。ちなみにExample
テストだけでもカバレッジが出るようです。
go test -v -cover
=== RUN Example
--- PASS: Example (0.00s)
=== RUN ExampleNewPerson
--- PASS: ExampleNewPerson (0.00s)
=== RUN ExamplePerson_GetFirstName
--- PASS: ExamplePerson_GetFirstName (0.00s)
=== RUN ExamplePerson_GetLastName
--- PASS: ExamplePerson_GetLastName (0.00s)
PASS
coverage: 100.0% of statements
ok github.com/taisa831/sandbox-go-impl/person 0.005s
Example
テストだけだと中途半端感があるので通常のテストに変えてみます。好みもあると思いますがGoland
やVSCode
の機能を使うと簡単にテストコードの枠組みが出力できるのでかなり楽にテストが作成できます。
func TestNewPerson(t *testing.T) {
type args struct {
firstName string
LastName string
}
tests := []struct {
name string
args args
want *Person
}{
{
"NewPerson",
args{"Taro", "Yamada"},
&Person{firstName: "Taro", lastName: "Yamada"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewPerson(tt.args.firstName, tt.args.LastName); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewPerson() = %v, want %v", got, tt.want)
}
})
}
}
func TestPerson_GetFirstName(t *testing.T) {
type fields struct {
FirstName string
LastName string
}
tests := []struct {
name string
fields fields
want string
}{
{
"GetFirstName",
fields{FirstName:"Taro", LastName:"Yamada"},
"Taro",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Person{
firstName: tt.fields.FirstName,
lastName: tt.fields.LastName,
}
if got := p.GetFirstName(); got != tt.want {
t.Errorf("GetFirstName() = %v, want %v", got, tt.want)
}
})
}
}
func TestPerson_GetLastName(t *testing.T) {
type fields struct {
FirstName string
LastName string
}
tests := []struct {
name string
fields fields
want string
}{
{
"GetLastName",
fields{FirstName:"Taro", LastName:"Yamada"},
"Yamada",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &Person{
firstName: tt.fields.FirstName,
lastName: tt.fields.LastName,
}
if got := p.GetLastName(); got != tt.want {
t.Errorf("GetLastName() = %v, want %v", got, tt.want)
}
})
}
}
同じようにカバレッジが出せました。
go test -v -cover
=== RUN TestNewPerson
=== RUN TestNewPerson/NewPerson
--- PASS: TestNewPerson (0.00s)
--- PASS: TestNewPerson/NewPerson (0.00s)
=== RUN TestPerson_GetFirstName
=== RUN TestPerson_GetFirstName/GetFirstName
--- PASS: TestPerson_GetFirstName (0.00s)
--- PASS: TestPerson_GetFirstName/GetFirstName (0.00s)
=== RUN TestPerson_GetLastName
=== RUN TestPerson_GetLastName/GetLastName
--- PASS: TestPerson_GetLastName (0.00s)
--- PASS: TestPerson_GetLastName/GetLastName (0.00s)
PASS
coverage: 100.0% of statements
ok github.com/taisa831/sandbox-go-impl/person 0.006s
どこのコードをテストが通っているかを確認したい場合にはhtml
に変換して確認することももちろんできます。
$ go test -coverprofile=cover.out
PASS
coverage: 100.0% of statements
ok github.com/taisa831/sandbox-go-impl/person 0.006s
$ go tool cover -html=cover.out -o cover.html
$ open cover.html
HTML出力結果
テストをスキップする
下記の例では--short
オプションと組み合わせて使っていますが、実行するとtesting.Short()
がtrue
となりスキップされます。ショートモードではこのテストは実行不要だという時などに使います。
func TestTimeConsuming(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
}
go test --short -v
=== RUN TestTimeConsuming
--- SKIP: TestTimeConsuming (0.00s)
testing_test.go:78: skipping test in short mode.
PASS
ok github.com/taisa831/sandbox-go/testing 0.005s
似た用途のメソッドはtesing
パッケージのTB
インターフェースに定義されているのであげておきます。基本的にはError
やErrorf
を使うことが多いと思います。
- Error(args ...interface{})
- ログを出力してエラーをマークする
- Errorf(format string, args ...interface{})
- フォーマットログを出力してエラーをマークする
- Fail()
- テストにエラーをマークする
- FailNow()
- テストにエラーをマークしてそのテストを終了する
- Failed() bool
- テストがエラーかをチェックする
- Fatal(args ...interface{})
- エラーを出力してそのテストを終了する
- Fatalf(format string, args ...interface{})
- フォーマットエラーを出力してそのテストを終了する
- Log(args ...interface{})
- ログを出力する
- Logf(format string, args ...interface{})
- フォーマットログを出力する
- Skip(args ...interface{})
- ログを出力してスキップをマークしてそのテストを終了する
- SkipNow()
- スキップをマークしてそのテストを終了する
- Skipf(format string, args ...interface{})
- フォーマットログを出力してスキップをマークしてそのテストを終了する
- Skipped() bool
- テストがスキップかをチェックする
サブテストとサブベンチマーク
1つのテスト関数にサブテストを追加して複数のテストを実行することができます。ベンチマークも同様に可能です。サブテストがすべて終わるまでt.Logf("%s", "finished")
は呼ばれないのでteardown
処理を入れることも可能です。
func TestFoo(t *testing.T) {
t.Run("A=1", func(t *testing.T) {
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
})
t.Run("A=2", func(t *testing.T) {
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
})
t.Run("B=1", func(t *testing.T) {
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
})
t.Logf("%s", "finished")
// teardown
}
go test -v --run Foo
=== RUN TestFoo
=== RUN TestFoo/A=1
=== RUN TestFoo/A=2
=== RUN TestFoo/B=1
--- PASS: TestFoo (0.00s)
--- PASS: TestFoo/A=1 (0.00s)
--- PASS: TestFoo/A=2 (0.00s)
--- PASS: TestFoo/B=1 (0.00s)
testing_test.go:114: finished
PASS
ok github.com/taisa831/sandbox-go/testing 0.005s
サブテストをパラレルに実行する
サブテスト内でt.Parallel()
を呼び出すことでサブテストをパラレルに実行することができます。パラレル動作を確認する為にA-1
テストにだけSleep
を入れて実行してみます。
func TestGroupedParallel(t *testing.T) {
tests := []struct {
Name string
Want bool
WantErr bool
}{
{
Name: "test1",
Want: true,
WantErr: false,
},
}
for _, tc := range tests {
tc := tc // capture range variable
t.Run(tc.Name, func(t *testing.T) {
t.Run("A=1", func(t *testing.T) {
t.Parallel()
got := math.Abs(-1)
time.Sleep(2 * time.Second)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
t.Logf("Len=2: %s", time.Now())
})
t.Run("A=2", func(t *testing.T) {
t.Parallel()
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
t.Logf("Len=2: %s", time.Now())
})
t.Run("B=1", func(t *testing.T) {
t.Parallel()
got := math.Abs(-1)
if got != 1 {
t.Errorf("Abs(-1) = %f; want 1", got)
}
t.Logf("Len=2: %s", time.Now())
})
})
}
t.Logf("%s", "finished")
// teardown
}
実行結果を見てみるとA-1
のテストが最後まで残りfinished
が呼ばれているのが分かります。パラレルテストでも全てのテストが終わるまでfinished
が呼ばれないのでteardown
処理をすることができます。
go test -v
=== RUN TestGroupedParallel
=== RUN TestGroupedParallel/test1
=== RUN TestGroupedParallel/test1/A=1
=== PAUSE TestGroupedParallel/test1/A=1
=== RUN TestGroupedParallel/test1/A=2
=== PAUSE TestGroupedParallel/test1/A=2
=== RUN TestGroupedParallel/test1/B=1
=== PAUSE TestGroupedParallel/test1/B=1
=== CONT TestGroupedParallel/test1/A=1
=== CONT TestGroupedParallel/test1/B=1
=== CONT TestGroupedParallel/test1/A=2
--- PASS: TestGroupedParallel (2.00s)
--- PASS: TestGroupedParallel/test1 (0.00s)
--- PASS: TestGroupedParallel/test1/B=1 (0.00s)
testing_test.go:149: Len=2: 2019-12-10 19:57:05.678677 +0900 JST m=+0.000551289
--- PASS: TestGroupedParallel/test1/A=2 (0.00s)
testing_test.go:141: Len=2: 2019-12-10 19:57:05.678767 +0900 JST m=+0.000641739
--- PASS: TestGroupedParallel/test1/A=1 (2.00s)
testing_test.go:133: Len=2: 2019-12-10 19:57:07.681258 +0900 JST m=+2.003074682
testing_test.go:153: finished
PASS
ok github.com/taisa831/sandbox-go/testing 2.009s
-run
オプションでテスト対象を絞る
-run
オプションを指定することでテストファイル内のテスト対象を絞ることができます。
# 全テスト対象
go test -run ''
# Fooにマッチするテストが対象
go test -run Foo
# FooにマッチかつサブテストA=にマッチするテストが対象
go test -run Foo/A=
# サブテストがA=1にマッチするテストが対象
go test -run /A=1
下記を実行するとFoo/A=
にマッチするテストだけ実行されてるのが分かります。
go test -v -run Foo/A=
=== RUN TestFoo
=== RUN TestFoo/A=1
=== RUN TestFoo/A=2
--- PASS: TestFoo (0.00s)
--- PASS: TestFoo/A=1 (0.00s)
--- PASS: TestFoo/A=2 (0.00s)
PASS
ok github.com/taisa831/sandbox-go/testing 0.005s
共通処理のエラー箇所を分かりやすく出力する
ちょっとしたことですが、テストで共通処理を呼び出している時、どのテストでエラーが出たかは共通処理のコード情報しか出力されないので通常だと分かりません。そういう時はhelper()
を使うと呼び出し元のテスト情報を出力してくれます。
必ずエラーとなるPreTest
関数を複数のテスト関数で呼び出して実行して確認してみます。また,T
とB
はtesting
のTB
のインターフェースを実装(正確にはcommon
が実装してそれを両方とも持っている)しているので*testing.T
も*testing.B
も渡すことができます。
func PreTest(tb testing.TB) {
got := math.Abs(-1)
if got != -1 {
// t.Helper()
tb.Errorf("Abs(-1) = %f; want 1", got)
}
}
通常だとPreTest
のエラー箇所である行情報しかでませんが、t.Helper()
のコメントアウトを外してもう一回実行してみます。
go test
--- FAIL: TestAbs (0.00s)
testing_test.go:155: Abs(-1) = 1.000000; want 1
--- FAIL: TestFoo (0.00s)
testing_test.go:155: Abs(-1) = 1.000000; want 1
testing_test.go:97: finished
呼び出し元のテスト側の行情報がでるようになりました。
go test
--- FAIL: TestAbs (0.00s)
testing_test.go:12: Abs(-1) = 1.000000; want 1
--- FAIL: TestFoo (0.00s)
testing_test.go:76: Abs(-1) = 1.000000; want 1
testing_test.go:97: finished
FAIL
exit status 1
FAIL github.com/taisa831/sandbox-go/testing 2.009s
前処理/後処理をする
*testing.M
を利用すると、対象のテストに対して「前処理」や「後処理」を書くことができます。テスト対象ファイルに下記のように記述することで実行が可能です。また、上記の--run
オプションで対象テストを絞っても「前処理」と」後処理」は実行されます。
func TestMain(m *testing.M) {
println("前処理")
m.Run()
println("後処理")
}
テストの最初と最後に「前処理」と「後処理」がプリントされました。
go test -v
前処理
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
=== RUN TestAbs2
--- PASS: TestAbs2 (0.00s)
=== RUN TestTimeConsuming
--- PASS: TestTimeConsuming (0.00s)
=== RUN TestFoo
=== RUN TestFoo/A=1
=== RUN TestFoo/A=2
=== RUN TestFoo/B=1
--- PASS: TestFoo (0.00s)
--- PASS: TestFoo/A=1 (0.00s)
--- PASS: TestFoo/A=2 (0.00s)
--- PASS: TestFoo/B=1 (0.00s)
=== RUN TestGroupedParallel
=== RUN TestGroupedParallel/test1
=== RUN TestGroupedParallel/test1/A=1
=== PAUSE TestGroupedParallel/test1/A=1
=== RUN TestGroupedParallel/test1/A=2
=== PAUSE TestGroupedParallel/test1/A=2
=== RUN TestGroupedParallel/test1/B=1
=== PAUSE TestGroupedParallel/test1/B=1
=== CONT TestGroupedParallel/test1/A=1
=== CONT TestGroupedParallel/test1/B=1
=== CONT TestGroupedParallel/test1/A=2
--- PASS: TestGroupedParallel (2.00s)
--- PASS: TestGroupedParallel/test1 (0.00s)
--- PASS: TestGroupedParallel/test1/B=1 (0.00s)
testing_test.go:160: Len=2: 2019-12-10 21:48:38.890123 +0900 JST m=+0.000768137
--- PASS: TestGroupedParallel/test1/A=2 (0.00s)
testing_test.go:152: Len=2: 2019-12-10 21:48:38.890183 +0900 JST m=+0.000827182
--- PASS: TestGroupedParallel/test1/A=1 (2.00s)
testing_test.go:144: Len=2: 2019-12-10 21:48:40.89206 +0900 JST m=+2.002648644
testing_test.go:164: finished
=== RUN ExampleHello
--- PASS: ExampleHello (0.00s)
=== RUN ExampleSalutations
--- PASS: ExampleSalutations (0.00s)
=== RUN ExamplePerm
--- PASS: ExamplePerm (0.00s)
PASS
後処理
ok github.com/taisa831/sandbox-go/testing (cached)
その他
testing
パッケージには他にもio
テストをしやすくする為のiotest
パッケージや、ブラックボックステスト用であるquick
パッケージ(ただし凍結されている模様)があります。
おわりに
Go
は標準でモックライブラリも用意しているので組み合わせれば標準でほとんどのテストができます。またhttptest
もあるのでe2e
テストもできます。これらをうまく使いこなして楽しい開発ライフを送りたいと思います。
余談ですが、本記事を書くにあたりGoDoc
をいろいろみていたら気になる箇所があったのでGo
本体にコミット&レビュー依頼してみたところ、何度かやりとりした後、無事取り込んでくれました。
GitHub
ではなくGerrit
を使っているので最初のセットアップは少し面倒ですが、一度セットアップすると以降は簡単にレビュー依頼をすることができます。下記の本家記事と日本語の記事がとても分かりやすいので是非参考にしてみてください。
参考
明日のテックタッチアドベントカレンダーの担当は @takakobem です!