Goのテストフレームワークを比較してみた

こんにちは。カヤックボンドの松本です。

この記事は 面白法人グループ Advent Calendar 2022 の5日目の記事です。

この枠ではテストフレームワークについての話をしようと思います。

きっかけ

あるプロジェクトのテストコードを見ていたら、こんな感じのテストコードを書かれていました。

func Test_Sample(t *testing.T) {
    ctx, usecase, user, err := initTest()
    if err != nil {
        t.Fatal(err)
    }

    tests := []struct {
        name         string
        request      *GetSelfRequest
        wantResponse *GetSelfResponse
        wantErr      error
    }{
        {
            name:    "success",
            request: &GetSelfRequest{},
            wantResponse: &GetSelfResponse{
                ID:    user.ID,
                Name:  user.Name,
                Money: user.Money,
                Exp:   user.Exp,
            },
            wantErr: nil,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, tErr := usecase.GetSelf(ctx, tt.request)

            if !errors.Is(tErr, tt.wantErr) {
                t.Fatalf("err:%v want: %v\n", tErr, tt.wantErr)
            }
            if tErr != nil {
                return
            }

            if got.ID != tt.wantResponse.ID {
                t.Errorf("got: %s, want: %s\n", got.ID, tt.wantResponse.ID)
            }
            if got.Name != tt.wantResponse.Name {
                t.Errorf("got: %s, want: %s\n", got.Name, tt.wantResponse.Name)
            }
            if got.Exp != tt.wantResponse.Exp {
                t.Errorf("got: %d, want: %d\n", got.Exp, tt.wantResponse.Exp)
            }
            if got.Level != tt.wantResponse.Level {
                t.Errorf("got: %d, want: %d\n", got.Level, tt.wantResponse.Level)
            }
        })
    }
}

// GetSelfRequest リクエスト
type GetSelfRequest struct {
}

// GetSelfResponse レスポンス
type GetSelfResponse struct {
    ID    UserID
    Name  string
    Money int
    Exp   int
}

「想定された戻り値と、取得できた戻り値」を一つひとつチェックするパターンです。

このような書き方をする場合、今後起きうる問題として「後から仕様変更があった場合に書き直すべきテストが検知できない」問題があります。

例えば、 GetSelfResponse にLevelというフィールドが増えた場合、Levelにどんな値が入っていてもテストが通過してしまいます。同じ人がテストを書き直すなら良いですが、忘れる可能性だって出てきますし、そもそも修正すべきテストは適切に落ちてほしいです。

こんな場合自分なりに知っている方法と、今回調べてみた手段を紹介しようと思います。

go-cmpで戦う

go-cmpreflect.DeepEqual() よりも高機能で、非公開なフィールドの取扱も細かく指定可能、更に差分が見やすいという特徴があります。

- if got.ID != tt.wantResponse.ID {
-      t.Errorf("got: %s, want: %s\n", got.ID, tt.wantResponse.ID)
-  }
-  if got.Name != tt.wantResponse.Name {
-      t.Errorf("got: %s, want: %s\n", got.Name, tt.wantResponse.Name)
-  }
-  if got.Exp != tt.wantResponse.Exp {
-      t.Errorf("got: %d, want: %d\n", got.Exp, tt.wantResponse.Exp)
-  }
-  if got.Level != tt.wantResponse.Level {
-      t.Errorf("got: %d, want: %d\n", got.Level, tt.wantResponse.Level)
-  }
+   if diff := cmp.Diff(got, tt.wantResponse); diff != "" {
+       t.Errorf("diff: %v\n", diff)
+   }

実行結果

diff:   &main.GetSelfResponse{
                ... // 2 identical fields
                Money: 10,
                Exp:   100,
        -       Level: 0,
        +       Level: 10,
          }

ただし、このライブラリは非公開なフィールドの扱いに対する指定が割と厳密で、 cmp.AllowUnexported() cmpopts.IgnoreUnexported() cmpopts.IgnoreField() 等が付いていないとpanicになります。歴史的経緯からか、何故かパッケージ名が違うところが面白いですね。

if diff := cmp.Diff(t.wantResponse, got, cmpopts.IgnoreUnexported(GetSelfResponse{})); diff != "" {
    t.Errorf("diff: %v\n", diff)
}

stretchr/testifyが人気らしい

他にも良いのが無いかなと思い調べてみたところ、今現在ではstretchr/testifyが割と人気な様子でした。v2も作ると書いており、今後の開発も盛んそうです。

こちらを使って書き直すと以下のようになります。

- if diff := cmp.Diff(t.wantResponse, got, cmp.IgnoreUnexported(GetSelfResponse{})); diff != "" {
-      t.Errorf("diff: %v\n", diff)
-  }
+   assert.Equal(t, tt.wantResponse, got)

実行結果

Error Trace:    /xxx/sample_test.go:84
Error:          Not equal: 
                expected: &main.GetSelfResponse{ID:"01855324-64ae-7c96-b456-c403ec90dc50", Name:"TestName", Money:10, Exp:100, Level:0}
                actual  : &main.GetSelfResponse{ID:"01855324-64ae-7c96-b456-c403ec90dc50", Name:"TestName", Money:10, Exp:100, Level:10}
                            
                Diff:
                --- Expected
                +++ Actual
                @@ -5,3 +5,3 @@
                  Exp: (int) 100,
                - Level: (int) 0
                + Level: (int) 10
                 })
Test:           Test_Sample/success

かなり美しい実行結果で、テストケースを作っているだけで無限にモチベーションが出てきそうな気がします。

(こちらのフレームワークでは非公開なフィールドは比較対象に含まれるようです。)

おわりに

テスト1つの書き方でも、本当にいろいろな書き方があるんだなあ、と思いました。

今現在は非公開フィールドについてどれくらい厳密に扱いたいかによって使用するフレームワークが決まってくるだろう、と感じました。

最後になりますが、未来の12/24のグループカレンダーでは別の方向からコード品質の向上に取り組んでおりました。よろしければこちらも是非ご覧ください!

techblog.kayac.com