AIにコーディングさせていたらテストが通らなくて、結局自力で解決した話です。
困っていたこと
- Goで外部APIを叩く実装があり、HTTPクライアントにRestyを使っていた
- 実際のAPIを叩くと正しい挙動が得られるが、モックサーバーを使ったテストが期待通りに動作してくれなかった
- テストコードをデバッグしたところ、JSON形式のレスポンスを返しているが
resty.R().SetResult()でRestyがJSONを構造体にアンマーシャルする部分が動いていなかった
原因
モックサーバーでContent-Typeのレスポンスヘッダーを設定していなかった
問題の状況
実装コード
Restyを使ってメールアドレスとパスワードから認証トークンを取得する実装です。
type AuthRequest struct { MailAddress string `json:"mailaddress"` Password string `json:"password"` } type AuthResponse struct { AuthToken string `json:"authToken"` } func (c *Client) getAuthToken(ctx context.Context) (string, error) { reqBody := &AuthRequest{ MailAddress: c.config.MailAddress, // test@example.com Password: c.config.Password, // password } var resBody AuthResponse var errRes map[string]string resp, err := c.restyClient.R(). SetContext(ctx). SetBody(reqBody). SetResult(&resBody). // ここで構造体にパースされる SetError(&errRes). Post("/v1/auth/token") if err != nil { return "", fmt.Errorf("HTTPリクエストに失敗しました: %w", err) } if resp.IsError() { return "", fmt.Errorf("APIエラー: status=%s, body=%v", resp.Status(), errRes) } return resBody.AuthToken, nil }
失敗したテスト
最初のテストコードでは、以下のようにモックサーバーを作成していました
// モックサーバーのセットアップ mux := http.NewServeMux() mux.HandleFunc("/v1/auth/token", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) // リクエストボディのテスト // レスポンス生成 w.WriteHeader(http.StatusOK) response := map[string]string{"authToken": "test-auth-token"} _ = json.NewEncoder(w).Encode(response) }) token, err := client.getAuthToken() require.NoError(t, err) assert.Equal(t, "test-auth-token", token)
テスト結果
Error: Not equal:
expected: "test-auth-token"
actual : ""
成功したテスト
テストのモックサーバーでContent-Typeヘッダーを明示的に設定することで解決しました
// モックサーバーのセットアップ mux := http.NewServeMux() mux.HandleFunc("/v1/auth/token", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method) // リクエストボディのテスト // レスポンス生成 w.Header().Set("Content-Type", "application/json") // 【追加】この行を追加することでレスポンスをJSONとして扱うようになる w.WriteHeader(http.StatusOK) response := map[string]string{"authToken": "test-auth-token"} _ = json.NewEncoder(w).Encode(response) }) token, err := client.getAuthToken() require.NoError(t, err) assert.Equal(t, "test-auth-token", token)
なぜContent-Typeが必要なのか
RestyのRequest.SetResultやRequest.SetErrorはレスポンスヘッダーのContent-Typeに基づいて構造体へのアンマーシャルが行われるため
Out of the box, Resty does response automatic unmarshaling for JSON and XML based on the response header Content-Type with methods Request.SetResult or Request.SetError are used.
感想
AIに書かせると実装が早いが、詰まったときのリカバリは返って時間が掛かって結局自分で書いたのと同じくらいの時間になりがち。AIに実装を任せることで細部への理解が甘くなるからこそ、テストが疎かになりがちな個人開発でもテストを書いてもらうようにしたい。
でも、このブログも50%くらいはAIに書いてもらったので、AIくんとはうまく付き合っていきたい。