【Go】go-cmpを利用した構造体のテスト手法
はじめに
今回はGoでの構造体の比較について、 google/go-cmp
を利用したテスト手法について紹介します。
今回のブログで使用したリポジトリ github.com
google/go-cmpとは
go-cmp とは値が等価かどうかを判別する package です。
等価を確認する方法に ==
や reflect.DeepEqual
が存在しますが、go-cmpはこれらより安全で強力な手法として紹介されています。
go-cmpが提供する機能は以下の通りです。
- 基本的な比較演算子ではない、カスタマイズされた等価機能を利用できる
- 型に Equal メソッドがある場合、比較時に Equal メソッドを使用する
- Equal メソッドがない場合、再帰的にプリミティブ型の値を比較する
再帰的な値の比較は reflect.DeepEqual
と動作が似ていますが、go-cmp ではデフォルトでは非公開なフィールド (Unexported Field) を比較しません。
その代わりに cmpopts.IgnoreUnexported
か AllowUnexported
を使用して、非公開なフィールドを比較するか無視するかをデベロッパーが決めることができます。
go-cmpで構造体のテストを実装する
まず、比較の対象に使われる構造体として Item
を定義します。
この構造体は全て公開されたフィールド (exported field) となっています。
type Item struct { ID string Name string }
次にテスト対象の関数として GetItem()
を定義します。
実装は構造体を生成して返すだけの単純なものです。
func GetItem(id, name string) Item { return Item{ ID: id, Name: name, } }
最後に go-cmp を使ってテストコードを実装します。
実装内容としては cmp.Diff
に比較したい2つの値を渡すと、差分が返ってきます。差分がある場合は、2つの値は等価ではないということになるため、テストを失敗させています。差分がない場合は、等価であるためテストが成功します。
got := GetItem(tc.args.id, tc.args.name) if diff := cmp.Diff(got, tc.want, opt); diff != "" { t.Fatalf("GetItem() = %v, want = %v", got, tc.want) }
Unexported field を持つ構造体の場合
次に非公開なフィールド (unexported field) を持つ構造体のテストをしてみます。
まず、構造体に非公開のフィールドを追加する必要があるので Item
に secretCode
を追加します。secretCode
は非公開なフィールドであるためフィールド名の先頭が小文字になります。
type Item struct { ID string Name string secretCode int64 }
非公開フィールドを比較する場合(AllowUnexported)
テストをする際に unexported field を比較対象にするには AllowUnexported
に非公開なフィールドを持つ構造体を渡します。
今回の例の場合 Item
を渡すとsecretCode
が比較の対象に含まれます。
Unexported fieldに対して比較対象にするか除外するかを設定しない場合、panicとなりテストが実行できません。
got := GetItem(tc.args.id, tc.args.name) opt := cmp.AllowUnexported(got) if diff := cmp.Diff(got, tc.want, opt); diff != "" { t.Fatalf("GetItem() = %v, want = %v", got, tc.want) }
非公開フィールドを除外する場合(cmpopts.IgnoreUnexported)
比較対象から除外したい場合はcmpopts.IgnoreUnexported
を使用します。これで非公開なフィールドを無視して安全な比較を行うことができます。
got := GetItem(tc.args.id, tc.args.name) opt := cmpopts.IgnoreUnexported(got) if diff := cmp.Diff(got, tc.want, opt); diff != "" { t.Fatalf("GetItem() = %v, want = %v", got, tc.want) }
【補足】reflect.DeepEqualでUnexported fieldを比較した場合
reflect.DeepEqual
は unexported fieldをデフォルトで比較対象にしているので、テストは何事もなく通ってしまいます。そのため、デベロッパーが意図しない比較が行われる場合があり安全ではありません。
got := GetItem(tc.args.id, tc.args.name) if !reflect.DeepEqual(got, tc.want) { t.Fatalf("GetItem() = %v, want = %v", got, tc.want) }
まとめ
今回はgo-cmpによる構造体のテスト方法を紹介しました。reflect.DeepEqualでは非公開フィールドも比較してしまい予期せぬ結果をまねくことも考えられます。 そのため、今回紹介した go-cmp
で構造体のテストをすることをおすすめします!