Goでは単体テストを書く場合に Table Driven Test が可読性、保守性の観点から推奨されています。
そのTableDrivenTestにおいてテストケースの書き方について備忘録です。
テストケースの書き方
ケースごとのstructを定義する
以下のようなコードを想定します。
type sampleUseCase struct{ field1 int field2 string } func TestHoge(t *testing.T){ cases := []sampleUseCases { { field1:1, filed2:"hoge", }, // 各テストケース } for _ , tt := range cases { // テストをゴリゴリ回す } }
Pros/Cons
- テストのユースケースがわかりやすい
- 一方でテストケースをいちいち外部に出さなくてもよく、Goぽくない。
- たまに公開structにしているケースもあるが、テストケースを公開structとして外部package向けにも公開するべきではない。
- 影響範囲を最小にするため
Testメソッドの中に非公開structを定義する
こちらが一般的な方法。コードは以下。
func TestHoge(t *testing.T){ cases := []strict{ field1 int field2 string }{ { field1: 1, field2: "hoge", }, // 各テストケース追加 } for _, tt := ranga cases { // テストをゴリゴリ回す } }
Pros/Cons
- GoらしいTableDrivenTest
- テストメソッド内で非公開structにしているので、外部への影響がない。
structのslice or map[string]struct
これは好みだと思いますが、ユニットテストを書くケースにおいてstructのsliceにするか、mapでテストケースを書くか2パターンがあると思います。
Goのテストではテストメソッドごとsubtestにしてテストするのがいいとされています。
理由は
- subtestの第一引数に「なんのテストを実行するのか」名前をつけられること
- deferが書きやすいこと
- リクエストなどを含まない単純なパターンのテストの場合、並列実行により高速が直列実行するときよりもテストのパフォーマンスを向上されらるため。
この1つ目のテストケースに名前をつけられる、というところですが具体的なコードだと以下のようになります。
func TestHoge(t *testing.T){ cases := []strict{ name string field1 int field2 string }{ { name: "test case 1" field1: 1, field2: "hoge", }, // 各テストケース追加 } for _, tt := ranga cases { tt := tt // サブテスト t.Run(tt.name, func(t *testing.T){ // テストを書く }) } }
これでも十分わかりやすいですが、テストの名前がわかるためだけに name
filedを増やすのも微妙だと思ってました。
そこで考えたのが、nameをkeyにした map[string]struct
を作成してテストケースにするプランです。
func TestHoge(t *testing.T){ cases := map[string]strict{ field1 int field2 string }{ "field1 case": { field1: 1, field2: "hoge", }, // 各テストケース追加 } for k, tt := ranga cases { tt := tt // サブテスト t.Run(k, func(t *testing.T){ // mapのkeyがそのままテストケースになる。 // テストを書く }) } }
このようにテストケースのstructの定義の仕方はいくつかありますが、mapが使いやすいので今後はmapでテストケースを書いていこうと思います。