👋

【Echo】Handlerのテストを共通化する

2022/07/09に公開

概要

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