【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.IgnoreUnexportedAllowUnexported を使用して、非公開なフィールドを比較するか無視するかをデベロッパーが決めることができます。

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) を持つ構造体のテストをしてみます。

まず、構造体に非公開のフィールドを追加する必要があるので ItemsecretCode を追加します。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 で構造体のテストをすることをおすすめします!

参考文献

godoc.org golang.org