👋
【Echo】Handlerのテストを共通化する
概要
GoのWebフレームワークechoにて、Handlerのテストを書く時に冗長になりがちなので共通化する。
冗長なパターン
例えば、以下のようなhandlerがあったとして
handler.go
type Handler interface {
ListNames(c echo.Context)
}
func New() Handler {
return &handler{}
}
type ListNamesRequest struct {
ID string `query:"id"`
}
type ListNamesResponse struct {
Names []string `json:"names"`
}
func (h *handler) ListNames(c echo.Context) error {
// 何かしら処理
r := &ListNamesRequest{}
if err := c.Bind(r); err != nil {
return err
}
return c.JSON(http.StatusOK, ListNamesResponse{
Names: []string{"hoge"},
})
}
そのままテストを書くとこうなる。
handler_test.go
func TestHandler_ListNames(t *testing.T) {
id := "userID"
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/userdata/list-names?id="+id, nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
ctrl := gomock.NewController(t)
ctx := c.Request().Context()
handler := New()
assert.NoError(t, handler.ListNames(c))
assert.Equal(t, http.StatusOK, rec.Code)
res := &ListNamesResponse{}
assert.NoError(t, json.Unmarshal(rec.Body.Bytes(), res))
assert.Equal(t, []string{"hoge"} ,res.Names)
}
上の方のhttptestの部分、毎回書くのが地味にキツい。
httptestの部分を関数にする
echoutil.go
type options struct {
path string
contentType string
}
type Option interface {
apply(*options)
}
func NewGetEchoTest(params url.Values, opts ...Option) (echo.Context, *httptest.ResponseRecorder) {
options := buildOptions(opts...)
target := options.path
if params != nil {
target += "?" + params.Encode()
}
return newEchoTest(context.Background(), httptest.NewRequest(http.MethodGet, target, nil), options)
}
func newEchoTest(ctx context.Context, req *http.Request, options *options) (echo.Context, *httptest.ResponseRecorder) {
e := echo.New()
req.Header.Set(echo.HeaderContentType, options.contentType)
rec := httptest.NewRecorder()
return e.NewContext(req.WithContext(ctx), recorder), rec
}
func buildOptions(ops ...Option) *options {
options := &options{
path: "/example",
contentType: echo.MIMEApplicationJSON,
}
for _, o := range ops {
o.apply(options)
}
return options
}
実際に使う
この関数を呼べば、httptest.NewRecorder、echo.Newの処理を共通化出来てスッキリする。
handler_test.go
func TestHandler_ListNames(t *testing.T) {
id := "userID"
c, rec := echoutil.NewGetEchoTest(url.Values{
"id": []string{id},
})
ctrl := gomock.NewController(t)
ctx := c.Request().Context()
handler := New()
assert.NoError(t, handler.ListNames(c))
assert.Equal(t, http.StatusOK, rec.Code)
res := &ListNamesResponse{}
assert.NoError(t, json.Unmarshal(rec.Body.Bytes(), res))
assert.Equal(t, []string{"hoge"} ,res.Names)
}
めっちゃスッキリした。
ついでに
postバージョンも書いておく。
echoutil.go
func NewPostEchoTest(params interface{}, opts ...Option) (echo.Context, *httptest.ResponseRecorder) {
options := buildOptions(opts...)
// paramsは構造体とか
b, _ := json.Marshal(params)
body = bytes.NewReader(b)
return newEchoTest(context.Background(), httptest.NewRequest(http.MethodPost, options.path, body), options)
}
Discussion