この記事は 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/