Go でテストコードを書くにあたり、標準パッケージである testing
パッケージの使い方を調べました。
環境
- go 1.19.2
コード
ファイル構造
. ├── go.mod ├── go.sum └── pkg └── num ├── num.go └── num_test.go
go module 作成
$ go mod init sample.com/sample/sample
./pkg/num/num.go
package num import ( "golang.org/x/xerrors" ) type Num struct { value int } func NewNum(value int) *Num { return &Num{ value: value, } } func (n Num) Double() (int, error) { if n.value == 0 { return 0, xerrors.New("not accepted") } return n.value * 2, nil }
./pkg/num/num_test.go
package num_test import ( "fmt" "testing" "sample.com/sample/sample/pkg/num" ) // helper function として利用する関数 func assertEqual(t *testing.T, got, want int) { // helper function である関数内で呼ぶと、helper funnction として適当な処理を付与してくれるメソッド // https://pkg.go.dev/testing#T.Helper t.Helper() if got != want { t.Errorf("expected %d, but %d", want, got) } } // シンプルなテストコード 正常性テスト func TestDouble1(t *testing.T) { in, want := 1, 2 x := num.NewNum(in) got, _ := x.Double() // helper function を利用 assertEqual(t, got, want) // 以下と同じ意 /* got, _ := x.Double() if got, _ := x.Double(); got != want { t.Errorf("expected %d, but %d", want, got) } */ } // シンプルなテストコード 異常性テスト func TestDouble2(t *testing.T) { in := 0 x := num.NewNum(in) if _, err := x.Double(); err == nil { t.Errorf("expected error") } } // Table Driven Test func TestDouble3(t *testing.T) { // テストケースとしたい定義をスライス内に宣言 cases := []struct { name string in int want int wantErr bool }{ { name: "normal", in: 1, want: 2, wantErr: false, }, { name: "abnormal", in: 0, want: 0, wantErr: true, }, } for _, tt := range cases { // イテレートされる各処理のスコープ内で、新たに変数を初期化してあげる tt := tt // 第一引数の名前でサブテストを実行してくれるメソッド // https://pkg.go.dev/testing#T.Run t.Run(tt.name, func(t *testing.T) { // *testing.T.Parallel を実行しておくと、サブテストを並行実行してくれる // https://pkg.go.dev/testing#T.Parallel t.Parallel() x := num.NewNum(tt.in) got, err := x.Double() if got != tt.want { t.Errorf("expected %d, but %d", tt.want, got) } if tt.wantErr { if err == nil { t.Errorf("expected error") } } }) } } // テスト全体の setup, teardown 関数を指定 func TestMain(m *testing.M) { setup() defer teardown() // *testing.M.Run で具体的なテスト関数を呼ぶ // https://pkg.go.dev/testing#M.Run m.Run() } // setup 関数 func setup() { fmt.Println("## setup func ##") } // teardown 関数 func teardown() { fmt.Println("## teardown func ##") } // ベンチマークテスト func BenchmarkDouble(b *testing.B) { in := 1 // *testing.B.N を指定すると、ベンチマークに充分な回数だけ処理を実行してくれる for i := 0; i < b.N; i++ { x := num.NewNum(in) x.Double() } } // GoDoc に反映 // Num 型の Double メソッドの Example として表示される func ExampleNum_Double() { x := num.NewNum(1) got, err := x.Double() if err != nil { fmt.Printf("%+v\n", err) } fmt.Println(got) // output: 2 }
テスト実行結果
go test
コマンドにテストしたいパッケージを指定して実行します。-v
で詳細表示。Go はデフォルトでテスト結果のキャッシュを持つようですが、-count=1
と付与すると、テスト結果のキャッシュを無効にできます。
The idiomatic way to disable test caching explicitly is to use -count=1.
$ go test -v -count=1 sample.com/sample/sample/pkg/num ## setup func ## === RUN TestDouble1 --- PASS: TestDouble1 (0.00s) === RUN TestDouble2 --- PASS: TestDouble2 (0.00s) === RUN TestDouble3 === PAUSE TestDouble3 === CONT TestDouble3 === RUN TestDouble3/normal === RUN TestDouble3/abnormal --- PASS: TestDouble3 (0.00s) --- PASS: TestDouble3/normal (0.00s) --- PASS: TestDouble3/abnormal (0.00s) === RUN ExampleNum_Double --- PASS: ExampleNum_Double (0.00s) PASS ## teardown func ## ok sample.com/sample/sample/pkg/num 0.002s
cmd go test - Build and test caching
基本的なお作法
Go にテストコードとして扱ってもらうためには、以下ルールを守るべし、とのことです。
- ファイル名を
XXX_test.go
とする - パッケージ名は、テスト対象コードのパッケージ名と同一とするか、
XXX_test
とする - 関数名を
TestXXX
とする - 関数は、引数として
*t testing.T
を持つシグネチャとなる
You write a test by creating a file with a name ending in _test.go that contains functions named TestXXX with signature func (t *testing.T). The test framework runs each such function; if the function calls a failure function such as t.Error or t.Fail, the test is considered to have failed.
パッケージ名については、以下の投稿が参考になります。同一パッケージ名とした場合、エクスポートされていない変数や関数にも、直接テストできる点がメリットとなります。
Test Code Package Comparison
・Black-box Testing: Use package myfunc_test, which will ensure you're only using the exported identifiers.
・White-box Testing: Use package myfunc so that you have access to the non-exported identifiers. Good for unit tests that require access to non-exported variables, functions, and methods.
Proper package naming for testing with the Go language
また、Go にはアサート文が用意されていません。その理由は以下と説明されています。アサート文があることが、逆にテスト結果の報告をわかりにくくしている、という思想でしょうか。なるほど、という感じ。
Go doesn't provide assertions. They are undeniably convenient, but our experience has been that programmers use them as a crutch to avoid thinking about proper error handling and reporting. Proper error handling means that servers continue to operate instead of crashing after a non-fatal error. Proper error reporting means that errors are direct and to the point, saving the programmer from interpreting a large crash trace. Precise errors are particularly important when the programmer seeing the errors is not familiar with the code.
go doc - Why does Go not have assertions?
アサート文を用意していない代わりに、helper function を使えと他記事で紹介されています。helper function については後述。
テストコードの具体的解説
具体的なテストコードの解説を、テスト関数毎に記述します。
シンプルなテストコード (TestDouble1, TestDouble2)
TestDouble1
と TestDouble2
がシンプルなテスト関数です。テスト対象コード内の処理を呼び、その処理結果を比較しているだけです。
なお、TestDouble1
では assertEqual
という関数を helper function として利用しています。helper function とは、ただの自作した関数です。テストケースに応じた適切な比較処理をする、エラーメッセージを返す関数を自作しよう、という事になります。
Table Driven Test (TestDouble3)
ある種のテストケースにおいては、Table Driven Test
の利用を推奨しています。以下が Table Driven Test
に関する説明です。
Writing good tests is not trivial, but in many situations a lot of ground can be covered with table-driven tests: Each table entry is a complete test case with inputs and expected results, and sometimes with additional information such as a test name to make the test output easily readable. If you ever find yourself using copy and paste when writing a test, think about whether refactoring into a table-driven test or pulling the copied code out into a helper function might be a better option.
Given a table of test cases, the actual test simply iterates through all table entries and for each entry performs the necessary tests. The test code is written once and amortized over all table entries, so it makes sense to write a careful test with good error messages.
具体的な Table Driven Test
の書き方は、スライス内で宣言した各テストケースの定義をイテラブルにテスト処理する、というものになります。そのために利用できる便利なメソッドが、testing
パッケージ内に用意されています。
Table Driben Test
において、*testing.T.Parallel()
を利用すると各テストを並行に実行してくれます。ただし注意点があり、以下の投稿で紹介されています。具体的には、for 内のスコープで変数を初期化してあげる必要があります。
Go言語のTable Driven Testでt.parallel()によるテスト並行実行時、一部のテストケースしか評価されない問題について
for 文と goroutines を併用した場合の動作に関して解説記事。
setup, tear down 処理の方法 (TestMain)
テスト全体の前後に処理を追加したい場合、func TestMain(m *testing.M)
という関数を作り、その中で *testing.M.run
メソッドを呼んであげます。TestMain
関数は、テストの main 関数の一部として実行され、*testing.M.run
メソッドが実際の各テスト関数を呼ぶメソッドとなります。そのため Testmain 関数内で setup と tear down にあたる処理を実行してあげれば、結果としてテスト全体の前後に処理を追加できるようになります。
TestMain runs in the main goroutine and can do whatever setup and teardown is necessary around a call to m.Run. m.Run will return an exit code that may be passed to os.Exit. If TestMain returns, the test wrapper will pass the result of m.Run to os.Exit itself.
テスト単体の前後に処理を追加したい場合、そのテスト関数内で実行させたい処理を呼んであげれば良いだけとなります。
ベンチマークテスト (BenchmarkDouble)
ベンチマークテストとして実行したい関数は func BenchmarkXxx(*testing.B)
というシグネチャで作成します。
-bench
オプションをつけて go test
コマンドを実行すると、ベンチマークテストの関数を実行してくれます。
$ go test -v -count=1 -bench=. -run=BenchmarkDouble sample.com/sample/sample/pkg/num ## setup func ## goos: linux goarch: amd64 pkg: sample.com/sample/sample/pkg/num cpu: Intel(R) Core(TM) i7-10510U CPU @ 1.80GHz BenchmarkDouble BenchmarkDouble-8 384570271 3.090 ns/op # <= 384570271 回処理されている PASS ## teardown func ## ok sample.com/sample/sample/pkg/num 1.512
カバレッジの取得
-cover
または -coverprofile
オプションをつけて go test
を実行すると、カバレッジ率を取得してくれます。
$$ go test -v -coverprofile=coverage.out sample.com/sample/sample/pkg/num ## setup func ## === RUN TestDouble1 --- PASS: TestDouble1 (0.00s) === RUN TestDouble2 --- PASS: TestDouble2 (0.00s) === RUN TestDouble3 === RUN TestDouble3/normal === PAUSE TestDouble3/normal === RUN TestDouble3/abnormal === PAUSE TestDouble3/abnormal === CONT TestDouble3/normal === CONT TestDouble3/abnormal --- PASS: TestDouble3 (0.00s) --- PASS: TestDouble3/normal (0.00s) --- PASS: TestDouble3/abnormal (0.00s) === RUN ExampleNum_Double --- PASS: ExampleNum_Double (0.00s) PASS coverage: 100.0% of statements # <= カバレッジ率が表示される ## teardown func ## ok sample.com/sample/sample/pkg/num 0.002s coverage: 100.0% of statements
-coverprofile
オプションで出力されたファイルを go tools
にかませると、出力結果を html にしてくれます。
# coverage.html というファイルが生成される $ go tool cover -html=coverage.out -o coverage.html
GoDoc に反映
テストコード内に記載したコードを、GoDoc 内に表示される Examle のコードとして扱えます。
Examples are compiled (and optionally executed) as part of a package’s test suite.
As with typical tests, examples are functions that reside in a package’s _test.go files. Unlike normal test functions, though, example functions take no arguments and begin with the word Example instead of Test
go blog - Testable Examples in Go
Examle としたいコードの関数を、以下の命名規則で作成します。
func ExampleFoo() // documents the Foo function or type func ExampleBar_Qux() // documents the Qux method of type Bar func Example() // documents the package as a whole
Examle のテスト関数は、コード内で標準出力された値を、コメントアウト部分の // output:
で指定された値と比較します。
As it executes the example, the testing framework captures data written to standard output and then compares the output against the example’s “Output:” comment. The test passes if the test’s output matches its output comment.
pkgsite
パッケージで表示を確認すると、以下の通り Example として表示されています。