年末年始はGoのテストコードを書いて過ごしている。
そんなことよりそろそろ2019年の抱負的なものを書かないと……とは思うものの、振り返り系記事ばかりで技術系記事がおろそかになってもあれなので、テストコードを書いていて学んだことをまとめておく。
[Go言語用VS Codeセットアップ手順]
実行環境はMac 10.14.2
+ VS Code 1.30.1
。
VS Codeの左側のメニューの5番目の四角いアイコンが「Extensions」、拡張機能のメニュー。検索ウィンドウに「go」と入力するとMicrosoft製のGo言語用拡張機能が表示されるので、これをインストールしVS Codeを再起動する。
拡張機能の説明欄にも「Generate unit tests skelton (using gotests)」とある。gotestsというライブラリに依存しているので、そちらもインストールしておく必要がある。
gotestsをインストールしていない場合にはターミナルでgo get -v github.com/cweill/gotests...
のコマンドを実行してインストールすることができる。
[テストコード・スケルトン自動生成]
上記の必要なソフトウェアのインストールが終わっていれば、Goのテストコードのスケルトンを自動生成する手順は以下の通り。
- 対象の関数名をクリックする
- command(⌘) + Shift(⇧) + Pでコマンドパレットを開く
(Windowsの場合:Ctrl + Shift + P) - コマンドパレットで「go test」と入力する
- 下矢印(↓) で「Go: Generate Unit Tests For Function」を選択しEnterキーを押す
これでテストコードが自動生成される。たとえば以下のような関数があった場合、
func myFunc(param string) string { // 省略 }
このようなテストコード・スケルトンが生成される。
func Test_myFunc(t *testing.T) { type args struct { param string } tests := []struct { name string args args want string }{ // TODO: Add test cases. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := myFunc(tt.args.param); got != tt.want { t.Errorf("myFunc() = %v, want %v", got, tt.want) } }) } }
// TODO: Add test cases.
の部分に以下のようにテストケースを追加していくことができる。
{ name: "hoge", args: args{ param: "hoge", }, want: "fuga", },
ただし、gotestsをインストールしていない場合は、コマンドパレットでコマンドを選択した際、ウィンドウ右下に以下のようにgotestsのインストールを促すアラートが表示される。
「Install」をクリックすると、インストールが開始される。下の画像のように「SUCCEEDED」と表示されたらインストール完了。
Goのテストは、sample.goのテストであればsample_test.goに記述する決まりとなっている。自動生成を実行したときにこのファイルが存在しない場合には、テストコードを記述するファイルの生成もまとめてやってくれる。
あとは生成されたスケルトンにテストデータを書き足すだけで、ユニットテストが完成する。
実際にやってみるとこんな感じ。めちゃくちゃ便利。
このプルリクエストを作成した時は、勉強だと思ってテストコードを写経していたが、自動生成を知っていればもっと本質的なテストデータを考える部分に集中できたと思う。
[関数の戻り値を比較する3つの方法]
テストの対象となる関数に戻り値がある場合、テストの期待値と戻り値を比較するには3つの方法がある。
等価演算子 (==, !=) で比較する
戻り値の型がstringやintなど比較可能な型であれば、等価演算子を利用して比較することができる。
先ほどの例でいうと関数myFunc
の戻り値の型はstring
なので、以下のように等価演算子を用いたスケルトンが生成される。
if got := myFunc(tt.args.param); got != tt.want { t.Errorf("myFunc() = %v, want %v", got, tt.want) }
比較可能な型についてはこちらが参考になる。構造体の値は、すべてのフィールドが比較可能であれば比較可能であり、スライス、マップ、関数の値は基本的には比較可能ではない。
Object.Equal() で比較する
time.Time
型のように、Equal()
やBefore()
、After()
などを独自の比較用関数をメソッドとして定義している型もある。これらを利用してテスト対象の関数の戻り値と期待値を比較することもできる。
time - The Go Programming Language
リフレクションを利用して reflect.DeepEqual() で比較する
基本的に比較可能ではないマップを戻り値とする以下のような関数に対してテストコードを自動生成すると、
func MyFunc() map[string]string { // 省略 }
以下のようになる。
func TestMyFunc(t *testing.T) { tests := []struct { name string want map[string]string }{ // TODO: Add test cases. } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := MyFunc(); !reflect.DeepEqual(got, tt.want) { t.Errorf("MyFunc() = %v, want %v", got, tt.want) } }) } }
ここでは、reflect.DeepEqual()
を用いて戻り値と期待値を比較している。Go 1.10.3でのdeepequal.go
の実装は以下のように、nil
やポインターの比較などをまとめて行ってくれる。
func DeepEqual(x, y interface{}) bool { if x == nil || y == nil { return x == y } v1 := ValueOf(x) v2 := ValueOf(y) if v1.Type() != v2.Type() { return false } return deepValueEqual(v1, v2, make(map[visit]bool), 0) } func deepValueEqual(v1, v2 Value, visited map[visit]bool, depth int) bool { if !v1.IsValid() || !v2.IsValid() { return v1.IsValid() == v2.IsValid() } if v1.Type() != v2.Type() { return false } // if depth > 10 { panic("deepValueEqual") } // for debugging // We want to avoid putting more in the visited map than we need to. // For any possible reference cycle that might be encountered, // hard(t) needs to return true for at least one of the types in the cycle. hard := func(k Kind) bool { switch k { case Map, Slice, Ptr, Interface: return true } return false } if v1.CanAddr() && v2.CanAddr() && hard(v1.Kind()) { addr1 := unsafe.Pointer(v1.UnsafeAddr()) addr2 := unsafe.Pointer(v2.UnsafeAddr()) if uintptr(addr1) > uintptr(addr2) { // Canonicalize order to reduce number of entries in visited. // Assumes non-moving garbage collector. addr1, addr2 = addr2, addr1 } // Short circuit if references are already seen. typ := v1.Type() v := visit{addr1, addr2, typ} if visited[v] { return true } // Remember for later. visited[v] = true } switch v1.Kind() { case Array: for i := 0; i < v1.Len(); i++ { if !deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) { return false } } return true case Slice: if v1.IsNil() != v2.IsNil() { return false } if v1.Len() != v2.Len() { return false } if v1.Pointer() == v2.Pointer() { return true } for i := 0; i < v1.Len(); i++ { if !deepValueEqual(v1.Index(i), v2.Index(i), visited, depth+1) { return false } } return true case Interface: if v1.IsNil() || v2.IsNil() { return v1.IsNil() == v2.IsNil() } return deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1) case Ptr: if v1.Pointer() == v2.Pointer() { return true } return deepValueEqual(v1.Elem(), v2.Elem(), visited, depth+1) case Struct: for i, n := 0, v1.NumField(); i < n; i++ { if !deepValueEqual(v1.Field(i), v2.Field(i), visited, depth+1) { return false } } return true case Map: if v1.IsNil() != v2.IsNil() { return false } if v1.Len() != v2.Len() { return false } if v1.Pointer() == v2.Pointer() { return true } for _, k := range v1.MapKeys() { val1 := v1.MapIndex(k) val2 := v2.MapIndex(k) if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) { return false } } return true case Func: if v1.IsNil() && v2.IsNil() { return true } // Can't do better than this: return false default: // Normal equality suffices return valueInterface(v1, false) == valueInterface(v2, false) } }
しかし関数の比較のときってポインターの比較とかやらないんだろうか……「Can't do better than this:」のコメントが気になる。
[まとめ]
VS Codeを使ってテストコード・スケルトンを自動生成して、効率的にテストを書いていきたい。
さて、とりあえず新年三日連続でブログを書いたので、三日坊主の条件は満たした。果たして……。
[2020/03/28追記]
こちらのブログで当記事に言及していただき、自動生成されるテストコードのテンプレートをカスタマイズできることを知った。go-cmpを使ったり、デバッグに%#vを利用したり、さらに便利になっていて凄い!
github.com
[2020/03/28追記ここまで]