以下の内容はhttps://handat.hatenablog.com/entry/20250920/1758364752より取得しました。


Restyのテストを書いたらJSONが構造体にアンマーシャルされず困った

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.SetResultRequest.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.

https://resty.dev/docs/response-auto-parse/

感想

AIに書かせると実装が早いが、詰まったときのリカバリは返って時間が掛かって結局自分で書いたのと同じくらいの時間になりがち。AIに実装を任せることで細部への理解が甘くなるからこそ、テストが疎かになりがちな個人開発でもテストを書いてもらうようにしたい。

でも、このブログも50%くらいはAIに書いてもらったので、AIくんとはうまく付き合っていきたい。




以上の内容はhttps://handat.hatenablog.com/entry/20250920/1758364752より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14