🐁
httptest.NewRequset で Go 1.22 で追加された PathValue の値を取得する
はじめに
Special Thanks Siketyan (@s6n_jp) !!!!!
教えてもらわなかったらたぶん解決できなかったです涙
以前こんな記事を書きましたが
私の検証不足ですが Go 1.22 で追加された Request.PathValue をうまく動かせませんでした。
とサボっていました。
その解決策を教えてもらったのでその紹介です。
※ 本記事は Go 1.22.3 での実装です。
go version
go version go1.22.3 darwin/arm64
func (*Request) PathValue
Go 1.22 で追加された機能です。
詳しくは以下の記事などをご参考ください。
/items/{id} のようにワイルドカードが使用できるようになりました。
func 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 (r *Request) PathValue(name string) string
はr.patIndex(name)
でヒットした場合に取得できる -
r.pat
やr.mathes
はfunc (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
で設定される
解決策
テストコードを下記のように修正します。
// 省略
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