STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

Go言語プロダクトでテストヘルパー関数を導入した話

はじめに

こんにちは、STORESの高田です

STORES には Go 言語で書かれたプロダクトがいくつかあります。今回は、その中で使用しているテストヘルパー関数である testutils.Assert と応用についてご紹介します

ベースは google/go-cmp

現在のコードベースでは、テスト中でデータの比較を行うのに github.com/google/go-cmp/cmp を使っています。google/go-cmp の使用例としては以下のようになります

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

次に testutils.Assert の本体ですが、非常に単純です。以下のようになっていて、

package testutils

func Assert(t *testing.T, want, got any, opts ...cmp.Option) {
    t.Helper()
    t.Run("Assert", func(t *testing.T) {
        t.Helper()
        if diff := cmp.Diff(want, got, opts...); diff != "" {
            t.Errorf("diff (-want +got):\n%s", diff)
        }
    })
}

使い方は次のようになっています

t.Run("...", func(t *testing.T) {
    ...
    opt := cmpopts.IgnoreFields(dbEmployee{}, "EntityID", "CreatedAt", "UpdatedAt")
    testutils.Assert(t, wantDBEmployee, gotEmployee, opt)
}

なぜヘルパー関数を作ったのか

これくらいなら共通化までしなくても良いのではないかと思われますが、実際のテストコード中では、下に挙げた例のように want/got の順序が逆になっていたり、t.Errorf() のメッセージが少し違ったりといったバリエーションが発生し、 go-cmp を使ったコードに一貫性を持たせるのは現実的に難しいように思いました

複数人でメンテナンスをしていると、この粒度まで厳密に揃えるのは現実的には難しいためヘルパー関数にしてしまっています

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

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

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

応用

単純な assert 以外に、テストケース内には特に「テーブルが期待した通り更新されているかどうか」のテストが多数ありました。テストの本筋から離れたエラー処理(SELECT に関するエラー処理など)も含めると、テストしたい内容に対して冗長になっていたため、 testutils.AssertTable ヘルパー関数を準備しました

generics を使って以下のようなものを準備すると、

package testutils

func AssertTable[T any](t *testing.T, db domain.DBConnector, name string, want []T, opts ...cmp.Option) {
    t.Helper()
    t.Run("AssertTable_"+name, func(t *testing.T) {
        t.Helper()
        query := fmt.Sprintf("SELECT %s FROM %s ORDER BY id ASC", columnsAs(*new(T), "", ""), name)

        var got []T
        if err := db.DB().Select(&got, query); err != nil {
            t.Error(err)
            return
        }

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

次のように使用できます

t.Run("...", func(t *testing.T) {
    ...
    testutils.AssertTable(t, db, "foo_tokens",
        []postgres.FooToken{
            {
                RefreshToken: "success-refresh-token-stores",
                AccessToken:  "new-access-token-success-refresh-token-stores",
            },
        },
        cmp.Options{
            cmpopts.IgnoreFields(postgres.FooToken{}, "ID", "AccessTokenExpiresAt"),
        })
}

いくつかのテーブルに対してテストを行うことを考えると、なかなか便利なヘルパー関数になっているように思います

ヘルパー関数内の処理は、プロダクトの実情に合わせて使いやすいものを準備できると、テストコードに一貫性を持たせられて見通しも良くなると思いますので、事例として参考にしてもらえればと思います