Eureka Engineering
Published in

Eureka Engineering

Go標準パッケージで作るシナリオテスト

Image by starline on Freepik

目次

  1. 既存のE2Eテストの振り返り
  2. 既存のE2Eテストの課題
  3. シナリオテストの導入
  4. シナリオテストの工夫
    4.1. 異常系
    4.2. 命名規則
  5. まとめ

1. 既存のE2Eテストの振り返り

func TestAPIHoge_PutFuga(t *testing.T) {
// テスト名から対応するsetup.sql, cleanup.sqlを探して実行する.
// 全てのテストケースで必要となるデータをセットアップする.
e2e.SetupDB(t)
t.Cleanup(func() {
e2e.CleanupDB(t)
})
tests := []struct {
body map[string]interface{}
want int
}{
{
body: map[string]any{
// body
},
want: http.StatusNoContent,
},
}
endpoint := "/hoge"
for _, tt := range tests {
t.Run(APITestName(endpoint, tt.want), func(t *testing.T) {
// テスト名から対応するsetup.sql, cleanup.sqlを探して実行する.
// テストケースごとに必要となるデータをセットアップする.
e2e.SetupDB(t)
t.Cleanup(func() {
e2e.CleanupDB(t)
})
r := e2e.NewRequest(http.MethodPut, endpoint, e2e.JSONBody(t, tt.body))
// Responseをgolden形式で比較する.
e2e.RunTest(t, r, tt.want)
})
}

2. 既存のE2Eテストの課題

// テスト名から対応するsetup.sql, cleanup.sqlを探して実行する.
e2e.SetupDB(t)
t.Cleanup(func() {
e2e.CleanupDB(t)
})
-- setup.sql
INSERT INTO `user` (`id`, `name`, `created_at`, `updated_at`)
VALUES (1, 'hoge', NOW(), NOW());
INSERT INTO `login_token` (`user_id, `token`, `expires_at`, `created_at`, `updated_at`)
VALUES (1, 'token', '2099-01-01 00:00:00', NOW(), NOW());

-- cleanup.sql
DELETE FROM `user` WHERE `id` = 1;
DELETE FROM `login_token` WHERE `user_id` = 1;

3. シナリオテストの導入

func TestAPIUser_Scenario(t *testing.T) {
var res map[string]any
t.Run("1 Create", func(t *testing.T) {
req := NewRequest("POST", "/users", e2e.JSONBody(map[string]any{"name": "John"}))
RunTest(t, req, http.StatusCreated, e2e.CaptureResponse(&res))
}
t.Run("2 Update", func(t *testing.T) {
req := NewRequest("PUT", fmt.Sprintf("/users/%s", res["id"], e2e.JSONBody(map[string]any{"name": "Taro"}))
RunTest(t, req, http.StatusNoContent)
}
t.Run("3 Get", func(t *testing.T) {
req := NewRequest("GET", fmt.Sprintf("/users/%s", res["id"], nil)
RunTest(t, req, http.StatusOK)
}
}
HTTP/1.1 200 OK
Connection: close
/* 中略 */
Content-Type: application/json; charset=utf-8
/* 中略 */
{
"id": 1,
- "name": "Taro"
+ "name": "John"
}
// 追加分.
func CaptureResponse(resp any) ResponseFilter {
return func(t *testing.T, r *http.Response) {
t.Helper()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
r.Body = io.NopCloser(bytes.NewReader(body))
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatal(err)
}
}
}

// ここから既存コード.
func RunTest(t *testing.T, r *http.Request, want int, filters …ResponseFilter) {
// 前略
for _, f := range filters {
f(t, got)
}
// 後略
}

type ResponseFilter func(t *testing.T, r *http.Response)

func NewRequest(method, endpoint string, body io.Reader, options …RequestOption) *http.Request {
r := httptest.NewRequest(method, endpoint, body)
for _, opt := range options {
opt(r)
}
return r
}
  1. ユーザー作成(http.StatusCreated)
  2. 1度目のname変更(http.StatusNoContent)
  3. ユーザー取得(http.StatusOK)
  4. 2度目のname変更(http.StatusBadRequest)
  5. メールアドレス登録(http.StatusNoContent)
  6. メールアドレス認証(http.StatusNoContent)
  7. 2度目のname変更(http.StatusNoContent)
omitted/
├─ routes.go
├─ api_user_test.go
├─ api_fuga_test.go
├─ testdata/
│ ├─ TestAPIUser/
│ │ ├─ 1_Create_201.golden
│ │ ├─ 2_Update_204_name.golden
│ │ ├─ 3_Get_200.golden
│ │ ├─ 4_Update_400_2nd_name_update.golden
│ │ ├─ 5_RegisterEmail_204.golden
│ │ ├─ 6_VerifyEmail_204.golden
│ │ ├─ 7_Update_200_retry_2nd_name_update.golden
│ │ ├─ cleanup.sql
│ │ ├─ setup.sql
│ ├─ TestAPIFuga/

--

--

Learn about Eureka’s engineering efforts, product developments and more.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store