🐁

httptest.NewRequset で Go 1.22 で追加された PathValue の値を取得する

2024/05/12に公開

はじめに

Special Thanks Siketyan (@s6n_jp) !!!!!
教えてもらわなかったらたぶん解決できなかったです涙

https://zenn.dev/otakakot/articles/93c028b9e1f074

以前こんな記事を書きましたが

私の検証不足ですが Go 1.22 で追加された Request.PathValue をうまく動かせませんでした。

とサボっていました。
その解決策を教えてもらったのでその紹介です。

※ 本記事は Go 1.22.3 での実装です。

go version
go version go1.22.3 darwin/arm64

func (*Request) PathValue

https://pkg.go.dev/net/http@master#Request.PathValue

Go 1.22 で追加された機能です。
詳しくは以下の記事などをご参考ください。

https://future-architect.github.io/articles/20240202a/

/items/{id} のようにワイルドカードが使用できるようになりました。

func NewRequest

https://pkg.go.dev/net/http/httptest#NewRequest

NewRequest returns a new incoming server Request, suitable for passing to an http.Handler for testing.

テストのための Request を構築してくれる関数です。
公式のサンプルコードでは以下のような使い方を提示してくれています。

package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
)

func main() {
	handler := func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "<html><body>Hello World!</body></html>")
	}

	req := httptest.NewRequest("GET", "http://example.com/foo", nil)
	w := httptest.NewRecorder()
	handler(w, req)

	resp := w.Result()
	body, _ := io.ReadAll(resp.Body)

	fmt.Println(resp.StatusCode)
	fmt.Println(resp.Header.Get("Content-Type"))
	fmt.Println(string(body))

}

パスパラメータ使った実装で httptest 使ってみた

テスト対象の実装
package handler

import (
	"encoding/json"
	"net/http"
	"sync"
)

type Item struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type Handler struct {
	mu    *sync.Mutex
	Items map[string]Item
}

func New() *Handler {
	return &Handler{
		mu:    &sync.Mutex{},
		Items: make(map[string]Item),
	}
}

func (hdl *Handler) GetItem(w http.ResponseWriter, r *http.Request) {
	hdl.mu.Lock()
	defer hdl.mu.Unlock()

	id := r.PathValue("id")

	item, ok := hdl.Items[id]
	if !ok {
		w.WriteHeader(http.StatusNotFound)

		return
	}

	if err := json.NewEncoder(w).Encode(item); err != nil {
		w.WriteHeader(http.StatusInternalServerError)

		return
	}

	w.WriteHeader(http.StatusOK)
}

上記コードに対してサンプルコードを鵜呑みにしてテストコードを以下のように書きます。

package handler_test

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/google/go-cmp/cmp"

	"github.com/otakakot/sample-go-httptest/handler"
)

func TestHandler_GetItem(t *testing.T) {
	t.Parallel()

	type fields struct {
		Items map[string]handler.Item
	}

	type args struct {
		w *httptest.ResponseRecorder
		r *http.Request
	}

	tests := []struct {
		name   string
		fields fields
		args   args
		status int
		want   handler.Item
	}{
		{
			name: "200 OK",
			fields: fields{
				Items: map[string]handler.Item{
					"1234": {
						ID:   "1234",
						Name: "item",
					},
				},
			},
			args: args{
				w: httptest.NewRecorder(),
				r: httptest.NewRequest(http.MethodGet, "/items/1234", nil),
			},
			status: http.StatusOK,
			want: handler.Item{
				ID:   "1234",
				Name: "item",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			hdl := handler.New()
			hdl.Items = tt.fields.Items // テスト用のデータを設定

			hdl.GetItem(tt.args.w, tt.args.r)

			res := tt.args.w.Result()

			if res.StatusCode != tt.status {
				t.Errorf("GetItem() status = %v, want %v", tt.args.w.Code, tt.status) // GetItem() status = 404, want 200

				return
			}

			got := handler.Item{}

			if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
				t.Fatal(err)
			}

			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Errorf("GetItem() mismatch (-want +got):\n%s", diff)
			}
		})
	}
}

こちらのテストは成功しません。 404 が返ってきます。
深掘りすると httptest.NewRequest ではパスパラメータの値が取得できないということに辿りつきます。

解決策

テストコードを下記のように修正します。

// 省略

func TestHandler_GetItem(t *testing.T) {
    // 省略

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            hdl := handler.New()
            hdl.Items = tt.fields.Items
    
-           hdl.GetItem(tt.args.w, tt.args.r)
+           mux := http.NewServeMux() 
+           mux.HandleFunc("GET /items/{id}", hdl.GetItem)
+           mux.ServeHTTP(tt.args.w, tt.args.r)
    
            res := tt.args.w.Result()
    
            if res.StatusCode != tt.status {
                t.Errorf("GetItem() status = %v, want %v", tt.args.w.Code, tt.status)
    
                return
            }
    
            got := handler.Item{}
    
            if err := json.NewDecoder(res.Body).Decode(&got); err != nil {
                t.Fatal(err)
            }
    
            if diff := cmp.Diff(tt.want, got); diff != "" {
                t.Errorf("GetItem() mismatch (-want +got):\n%s", diff)
            }
        })
    }
}

パスパラメータの設定を利用するためには ServeHTTP() を使う必要があったのですね。

おわりに

これで httptest を使って Go 1.22 で追加された PathValue も対応できますね!
いちいち httptest.NewServer() でサーバーを建てる必要がなくなりました!

Discussion