My External Storage

May 8, 2020 - 4 minute read - Comments - go

[Go] 一部のフィールドを無視して構造体を比較したいときはgo-cmpを使う

Goでテストを書くとき、期待値として構造体を比較したいときは多々ある。
時刻情報など、構造体の一部のフィールドの値だけ無視して構造体オブジェクトを比較する方法をまとめた。

TL;DR

import (
  "time"
  "testing"

  "github.com/google/go-cmp/cmp"
  "github.com/google/go-cmp/cmp/cmpopts"
)

type Foo strcut{
    Name      string
    Timestamp time.Time
}

func TestLogger(t *testing.T) {
  var got Foo
  // Arrange, Action...

  // Assert。TimeStampフィールドは無視して比較する
  if d := cmp.Diff(got, want, cmpopts.IgnoreFields(got, "TimeStamp")); len(d) != 0 {
    t.Errorf("differs: (-got +want)\n%s", d)
  }
}

構造体の一部を無視して比較したい

Goのテストを書いていると、ある構造体が期待通りの値になったか確認したくなるだろう。 その中で一部の値は無視して期待値と比較をしたくなるときがある。

  • DB保存時に自動裁判されるIDは無視して比較したい
  • CreatedAtのような時刻情報は無視して比較したい

たとえば、次のような構造体をDBから取得するテストを書く場合、IDCreatedAtModifiedAtのフィールドを無視したいだろう。 大抵の場合、DBで自動採番されたIDやデータの作成時刻などはテストケースで検証すべきことではないからだ。

type SimpleObject struct {
  ID         int       `json:"id" db:"id"`
  Name       string    `json:"name" db:"name"`
  Address    string    `json:"address" db:"address"`
  CreatedAt  time.Time `json:"created_at" db:"created_at"`
  ModifiedAt time.Time `json:"modified_at" db:"modified_at"`
}

愚直に書いてしまうと、次のようなコードになってしまう。

func TestSimple(t *testing.T) {
  var got SimpleObject
  var want SimpleObject
  // Arrange, Action...

  // Assert ID、CreatedAt、ModifiedAt以外のフィールドの値を検証していく。
  if got.Name != want.Name {
    t.Errorf("want %v, but got %v", want.Name, got.Name)
  }
  if got.Address != want.Address {
    t.Errorf("want %v, but got %v", want.Address, got.Address)
  }
}

複雑な構造体ほど比較すべき項目が増えていってしまう。 このようなとき、go-cmpライブラリを使うとすっきりした検証処理を記載できる。

go-cmpを使うと任意のフィールドを除外して等価性を評価できる

go-cmpは複雑な構造体オブジェクト同士の等価性や差分を取得するためのライブラリだ。

次のようにDiff関数で構造体を比較を実行する。

if diff := cmp.Diff(want, got); diff != "" {
  t.Errorf("MakeGatewayInfo() mismatch (-want +got):\n%s", diff)
}

構造体の値に差分があった場合、次のような文字列出力が得られる。

MakeGatewayInfo() mismatch (-want +got):
  cmp_test.Gateway{
    SSID:      "CoffeeShopWiFi",
-   IPAddress: "192.168.0.2",
+   IPAddress: "192.168.0.1",
    NetMask:   net.IPMask{0xff, 0xff, 0x00, 0x00},
    Clients: []cmp_test.Client{
    ...

IgnoreFieldsオプションを使って特定のフィールドを無視する

go-cmpは単純に構造体を比較するだけでなく、オプションを使ってさまざまな比較をすることができる。 そのなかの1つがIgnoreFieldsオプションだ。
IgnoreFieldsを渡してDiff関数を実行すると、IgnoreFieldsで指定されたフィールドの値を無視して比較を行なうことができる。

使い方は簡単で、Diff関数実行時に無視して欲しいフィールド名を渡すだけだ。 オプションなしで次のようなテストコードを書いてみる。

// https://play.golang.org/p/m0K3Ub5LSPY
package main

import (
  "testing"

  "github.com/google/go-cmp/cmp"
)

type SimpleObject struct {
  ID        int
  Name      string    `json:"name" db:"name"`
  Address   string    `json:"address" db:"address"`
}

func TestSimple(t *testing.T) {
  got := &SimpleObject{
    ID:   100,
    Name: "same name",
  }
  want := &SimpleObject{
    ID:   200,
    Name: "same name",
  }

  if d := cmp.Diff(got, want); len(d) != 0 {
    t.Errorf("differs: (-got +want)\n%s", d)
  }
}

この場合、IDが異なるので当然結果はエラーになる。

=== RUN   TestSimple
    TestSimple: prog.go:29: differs: (-got +want)
          &main.SimpleObject{
        -   ID:        100,
        +   ID:        200,
            Name:      "same name",
            Address:   "",
            CreatedAt: s"0001-01-01 00:00:00 +0000 UTC",
          }
--- FAIL: TestSimple (0.00s)
FAIL

ここで、IgnoreFieldsを使ってIDフィールドを無視する設定を書く。 こうすると、IDフィールド以外は差分がないので、テストがパスするようになる。

// https://play.golang.org/p/h1uFWBxUqxc
if d := cmp.Diff(got, want, cmpopts.IgnoreFields(*got, "ID")); len(d) != 0 {
  t.Errorf("differs: (-got +want)\n%s", d)
}

また、IgnoreFieldsはネストした構造体のフィールドにも適用できる。

type Foo struct{
  Simple *SimpleObject
}

type SimpleObject struct {
  ID      int
  Name    string `json:"name" db:"name"`
  Address string `json:"address" db:"address"`
}

Foo構造体にネストしている*SimpleObjectIDフィールドを無視してFooオブジェクトを比較したいときは次のようになる。

cmpopts.IgnoreFields(Foo{}, "Simple.ID")

終わりに

go-cmpライブラリ自体は昔から使っていたのだが、先日IgnoreFieldsオプションをはじめて知った。
ライブラリのサブパッケージまでちゃんと確認していなかったのが、原因だ。
思わぬ使い方があったり、または誤用してしまう可能性もあるため、ライブラリのGoDocはひととおり読んで使っていきたい。

参考

関連記事