every Tech Blog

株式会社エブリーのTech Blogです。

DELISH KITCHENのユニットテストで使用しているライブラリ

この記事は every Tech Blog Advent Calendar 2024(夏) 7日目の記事です。

はじめに

エブリーでソフトウェアエンジニアをしている本丸です。
Go Conference 2024もいよいよ明日開催ですね。
Goに関する話ということでDELISH KITCHENのユニットテストで使用されているライブラリを紹介したいと思います。

弊社ブログの過去の記事にテストの可読性についてのものがあるので興味があればぜひ読んでみてください!
Go testにおける可読性を保つ方法を考える

DELISH KITCHENのユニットテストで使用しているライブラリ

DELISH KITCHENではユニットテストを行うときに主に以下の4つのライブラリを使用しています。

  • gomock
  • testify/assert
  • go-cmp
  • httptest

gomock

名前の通り、ユニットテストの際にモックを提供してくれます。
https://github.com/uber-go/mock

gomockでは以下のようなinterfaceからmockを生成することができ、

type UserRepo interface {
    Insert(age int) User
    BulkInsert(ages []int) []*User
    Change(u User) *User
}

テストコードの中で下記のように使います。

   ctrl := gomock.NewController(t)
    repo := NewMockUserRepo(ctrl)
    repo.EXPECT().Add(20).Return(User{})

これだけでも便利なのですが、社内のコードに個人的に便利な機能だと思うものがあったので、いくつか紹介します。

Do()

ドキュメントからの引用ですが、下記のことを行ってくれます。

Doは、呼び出しがマッチしたときに実行するアクションを宣言します。後方互換性を保つため、関数の戻り値は無視されます。

社内で具体的にどのように使っているかというと

   repo.EXPECT().Change(gomock.Eq(u)).
        Do(func(user) { u.ID = 1 }).
        Return(&u)

構造体を受け取って、その構造体のフィールドを変更して返す関数のモックを含む時に使用しています。

InAnyOrder()

こちらもドキュメントからの引用ですが、下記のことを行ってくれます。

InAnyOrderは、順序を無視して同じ要素のコレクションに対して真を返すMatcherです。

社内で具体的にどのように使っているかというと

idMap := map[int]struct{}{
    19: struct{}{},
    20: struct{}{},
}
ids := make([]int, 0, len(idMap))
for id := range idMap {
    ids = append(ids, id)
}

repo.BulkInsert()

のような処理があり、BulkInsert()のmockを作りたい時に

   repo.EXPECT().BulkInsert(gomock.InAnyOrder([]int64{20, 19})).
    Return()

arrayの順番が保証されないためこちらを使用しています。

testify/assert

ある値がこうなるはずだというアサーションのチェックを行ってくれます。
https://github.com/stretchr/testify

testify/assertを使用してある関数のレスポンスが期待したものと一致するか確認したい場合は以下のようになります。

   want := true
    got := doAnything()
    assert.Equal(t, want, got)

様々なアサーションが用意されているのですが、Equal以外では下記に示したものがDELISH KITCHENだとよく使用されていました。

  • Contains()
  • Error()
  • Len()

go-cmp

オブジェクトを比較してくれるライブラリで、ユニットテストでもオブジェクトの比較のために使用しています。
https://github.com/google/go-cmp

   if diff := cmp.Diff(want, got); len(diff) != 0 {
        t.Errorf("got diff = %v", diff)
    }

go-cmpはオプションを使用することで様々なケースに対応することが可能です。 その中から社内で使われているものを一部紹介します。

IgnoreUnexported

IgnoreUnexportedをオプションとして指定すると、構造体の中のprivateなフィールドなどunexportedなものを無視して比較してくれます。

type SearchRequest struct {
    Client http.Client
    url    string
}

例えば、上記のような構造体があった場合はClientだけ比較されて、urlは無視されるといった挙動になります。

IgnoreFields

IgnoreFieldをオプションとして指定すると、構造体の中の指定したフィールドを無視して比較してくれます。

type User struct{
    ID        int
    CreatedAt time.Time
}

opts := []cmp.Option{
    cmpopts.IgnoreFields(User{}, "CreatedAt"),
}

上記のように指定すると、CreatedAtが無視されてIDだけ比較されるという挙動になります。

SortSlices

SortSlicesをオプションとして指定すると、指定したarrayのフィールドをソートした後に比較してくれます。

func GetUserIDs() []int {
    // 要素の順番がランダムなuserIDのarrayを返す処理
}

got := GetUserIDs()
want := []int{1, 2, 3}

opt := cmpopts.SortSlices(func(i, j int) bool {
    return i < j
})
if diff := cmp.Diff(got, want, opt); diff != "" {
    t.Errorf("GetUserIDs() = %v, want %v", got, tt.want)
}

例えば、GetUserIDs()という関数のテストをしたい時に、実際のコードではランダムな順序で問題ない場合でもテストでは順序も含めて比較を行うため失敗してしまうということが起こり得ます。このような時にSortSlicesをオプションとして指定すると任意の順番にソートした後に比較を行うため、配列の順番でテストが失敗するということは起こらなくなります。

httptest

標準ライブラリなので趣旨と少しズレるかもしれませんが、テスト用のモックサーバーとして利用しています。

func NewRequestHTTPRequestMock() (*httptest.Server, func() string) {
    var body string
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        b, _ := io.ReadAll(r.Body)
        body = string(b)
    })), func() string {
        return body
    }
}

func Test_Request(t *testing.T) {
    requestMock, getRequestContent := NewRequestHTTPRequestMock()
    _, _ = s.Search(requestMock.URL)
    got := getRequestContent()
    if diff := cmp.Diff(tt.want, got); diff != "" {
        t.Errorf("got diff (-want +got):\n%s", diff)
    }    
}

DELISH KITCHENではhttp.Clientを使用している箇所があり、利用箇所のテストを行うためにhttptestを利用しています。 上記のコードでは、NewRequestHTTPRequestMock()でモックサーバーを作成して、そこに対してリクエストを行うことでリクエストの中身が正しいのかのテストを行なっています。

まとめ

改めてまとめてみるとDELISH KITCHENではGoのテストではメジャーなライブラリが使われているといった印象でした。それと同時に、普段使用しているライブラリでも改めてドキュメントを読み直してみると、自分は使いこなせていない部分も多いと気付かされました。

Go Conference 2024 まで、あと1日!
https://gocon.jp/2024/

株式会社エブリー は、Platinum Gold スポンサーとして Go Conference 2024 に参加します。 ぜひ、ブースやセッションでお会いしましょう!
https://gocon.jp/2024/sponsors/2/