$30 off During Our Annual Pro Sale. View Details »

CustomMiddlewareのテストをする方法

 CustomMiddlewareのテストをする方法

GoのWebFrameworkのechoを使ったカスタムミドルウェアの作り方とテスト方法についての発表です。

サンプルコード
https://github.com/saicologic/echo_custom_middleware_sample

Satoru Mikami

December 14, 2022
Tweet

More Decks by Satoru Mikami

Other Decks in Technology

Transcript

  1. CustomMiddleware
    のテストを
    する方法

    View Slide

  2. 自己紹介
    Satoru Mikami
    Backend Engineer at Voicy
    Twitter: @saicologic
    Go
    と私
    Go
    で開発して2
    年になりました。
    Go Conference 2022 Spring
    に登壇しました
    来年に向けてプロポーザルを考え中
    スライドは、slidev
    で作りました。

    View Slide

  3. CustomMiddleware
    ミドルウェアとは、ざっくりいうとHTTP
    のリクエスト/
    レスポンスに共通の処理を加えるところ
    連鎖する関数で指定した順番で実行します。
    弊社で利用しているWebFramework
    のecho
    を例に紹介します。
    標準でも色々あります
    Basic Auth
    CORS
    CSRF
    Rate Limiter
    Recover
    などなど
    https://echo.labstack.com/middleware/

    View Slide

  4. ユースケース
    カスタムヘッダーの追加
    Context
    の追加
    request_id
    user_id
    全てのリクエストとレスポンスのログ出力
    受け取ったErr
    の内容を見てログ出力レベルを返す
    IP
    フィルター
    Monitoring Service
    のTracer
    追加

    View Slide

  5. ミドルウェアとテスト
    レスポンス結果
    verbose
    今回は、リクエストヘッダーで受け取ったUserID
    をContext
    に格納する例を紹介します
    1 $ curl -v http://localhost:1323 -H "X-USER-ID:12345678"
    1 12345678
    1 * Trying 127.0.0.1:1323...
    2 * Connected to localhost (127.0.0.1) port 1323 (#0)
    3 > GET / HTTP/1.1
    4 > Host: localhost:1323
    5 > User-Agent: curl/7.79.1
    6 > Accept: */*
    7 > X-USER-ID:12345678

    View Slide

  6. main.go
    1 // Handler
    2 func GetUser(c echo.Context) error {
    3 ctx := GetContextValues(c)
    4 return c.String(http.StatusOK, ctx.UserID)
    5 }
    6
    7 func initEcho() *echo.Echo {
    8 e := echo.New()
    9 e.Use(setUserIDMiddleware)
    10 e.GET("/", GetUser)
    11 return e
    12 }
    13
    14 func main() {
    15 e := initEcho()
    16 e.Logger.Fatal(e.Start(":1323"))
    17 }

    View Slide

  7. Package import
    1 import (
    2 "net/http"
    3 echo "github.com/labstack/echo/v4"
    4 )

    View Slide

  8. main
    15 e := initEcho()
    1 // Handler
    2 func GetUser(c echo.Context) error {
    3 ctx := GetContextValues(c)
    4 return c.String(http.StatusOK, ctx.UserID)
    5 }
    6
    7 func initEcho() *echo.Echo {
    8 e := echo.New()
    9 e.Use(setUserIDMiddleware)
    10 e.GET("/", GetUser)
    11 return e
    12 }
    13
    14 func main() {
    16 e.Logger.Fatal(e.Start(":1323"))
    17 }

    View Slide

  9. initEcho
    テストで使う共通で使う
    7 func initEcho() *echo.Echo {
    8 e := echo.New()
    9 e.Use(setUserIDMiddleware)
    10 e.GET("/", GetUser)
    11 return e
    12 }
    1 // Handler
    2 func GetUser(c echo.Context) error {
    3 ctx := GetContextValues(c)
    4 return c.String(http.StatusOK, ctx.UserID)
    5 }
    6
    13
    14 func main() {
    15 e := initEcho()
    16 e.Logger.Fatal(e.Start(":1323"))
    17 }

    View Slide

  10. setUserIDMiddleware
    テストで使う共通で使う
    9 e.Use(setUserIDMiddleware)
    1 // Handler
    2 func GetUser(c echo.Context) error {
    3 ctx := GetContextValues(c)
    4 return c.String(http.StatusOK, ctx.UserID)
    5 }
    6
    7 func initEcho() *echo.Echo {
    8 e := echo.New()
    10 e.GET("/", GetUser)
    11 return e
    12 }
    13
    14 func main() {
    15 e := initEcho()
    16 e.Logger.Fatal(e.Start(":1323"))
    17 }

    View Slide

  11. setUserIDMiddleware
    1 func setUserIDMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    2 return func(c echo.Context) error {
    3 userID := c.Request().Header.Get("X-USER-ID")
    4 ctx := GetContextValues(c)
    5 ctx.UserID = userID
    6 return next(c)
    7 }
    8 }

    View Slide

  12. setUserIDMiddleware
    4 ctx := GetContextValues(c)
    1 func setUserIDMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    2 return func(c echo.Context) error {
    3 userID := c.Request().Header.Get("X-USER-ID")
    5 ctx.UserID = userID
    6 return next(c)
    7 }
    8 }

    View Slide

  13. GetContextValues
    9 func GetContextValues(c echo.Context) *ContextValues {
    10 values, ok := c.Get(string(ContextValuesKey)).(*ContextValues)
    11 if values == nil || !ok {
    12 values = &ContextValues{}
    13 c.Set(string(ContextValuesKey), values)
    14 }
    15 return values
    16 }
    1 type ContextValues struct {
    2 UserID string
    3 }
    4
    5 type contextValuesKey string
    6
    7 const ContextValuesKey = contextValuesKey("ContextValuesKey")
    8

    View Slide

  14. github.com/labstack/echo/v4/context.go
    1 type (
    2 // Context represents the context of the current HTTP request. It holds request and
    3 // response objects, path, path parameters, data and registered handler.
    4 Context interface {
    5
    省略
    6 // Get retrieves data from the context.
    7 Get(key string) interface{}
    8
    9 // Set saves data in the context.
    10 Set(key string, val interface{})
    11 }
    12 )

    View Slide

  15. GetContextValues
    1 type ContextValues struct {
    2 UserID string
    3 }
    4
    5 type contextValuesKey string
    6
    7 const ContextValuesKey = contextValuesKey("ContextValuesKey")
    8
    9 func GetContextValues(c echo.Context) *ContextValues {
    10 values, ok := c.Get(string(ContextValuesKey)).(*ContextValues)
    11 if values == nil || !ok {
    12 values = &ContextValues{}
    13 c.Set(string(ContextValuesKey), values)
    14 }
    15 return values
    16 }

    View Slide

  16. GetUser
    2 func GetUser(c echo.Context) error {
    3 ctx := GetContextValues(c)
    4 return c.String(http.StatusOK, ctx.UserID)
    5 }
    1 // Handler
    6
    7 func initEcho() *echo.Echo {
    8 e := echo.New()
    9 e.Use(setUserIDMiddleware)
    10 e.GET("/", GetUser)
    11 return e
    12 }
    13
    14 func main() {
    15 e := initEcho()
    16 e.Logger.Fatal(e.Start(":1323"))
    17 }

    View Slide

  17. main_test.go
    1 func TestCustomHandler(t *testing.T) {
    2 t.Run("Add userID", func(t *testing.T) {
    3 userID := "12345678"
    4 method := echo.GET
    5 target := "/"
    6 body := strings.NewReader("")
    7 req := httptest.NewRequest(method, target, body)
    8 req.Header.Set("X-USER-ID", userID)
    9 rec := httptest.NewRecorder()
    10
    11 e := initEcho()
    12 e.ServeHTTP(rec, req)
    13
    14 ctx := e.AcquireContext()
    15 defer e.ReleaseContext(ctx)
    16 c := GetContextValues(ctx)
    17 res := rec.Result()
    18 assert.Equal(t, 200, res.StatusCode)
    19 assert.Equal(t, userID, rec.Body.String())
    20 assert.Equal(t, userID, c.UserID)
    21 })
    22 }

    View Slide

  18. Package import
    1 import (
    2 "net/http/httptest"
    3 "strings"
    4 "testing"
    5
    6 echo "github.com/labstack/echo/v4"
    7 "github.com/stretchr/testify/assert"
    8 )

    View Slide

  19. TestCustomHandler
    3 userID := "12345678"
    1 func TestCustomHandler(t *testing.T) {
    2 t.Run("Add userID", func(t *testing.T) {
    4 method := echo.GET
    5 target := "/"
    6 body := strings.NewReader("")
    7 req := httptest.NewRequest(method, target, body)
    8 req.Header.Set("X-USER-ID", userID)
    9 rec := httptest.NewRecorder()
    10
    11 e := initEcho()
    12 e.ServeHTTP(rec, req)
    13
    14 ctx := e.AcquireContext()
    15 defer e.ReleaseContext(ctx)
    16 c := GetContextValues(ctx)
    17 res := rec.Result()
    18 assert.Equal(t, 200, res.StatusCode)
    19 assert.Equal(t, userID, rec.Body.String())
    20 assert.Equal(t, userID, c.UserID)
    21 })
    22 }

    View Slide

  20. TestCustomHandler
    4 method := echo.GET
    5 target := "/"
    6 body := strings.NewReader("")
    7 req := httptest.NewRequest(method, target, body)
    8 req.Header.Set("X-USER-ID", userID)
    9 rec := httptest.NewRecorder()
    1 func TestCustomHandler(t *testing.T) {
    2 t.Run("Add userID", func(t *testing.T) {
    3 userID := "12345678"
    10
    11 e := initEcho()
    12 e.ServeHTTP(rec, req)
    13
    14 ctx := e.AcquireContext()
    15 defer e.ReleaseContext(ctx)
    16 c := GetContextValues(ctx)
    17 res := rec.Result()
    18 assert.Equal(t, 200, res.StatusCode)
    19 assert.Equal(t, userID, rec.Body.String())
    20 assert.Equal(t, userID, c.UserID)
    21 })
    22 }

    View Slide

  21. TestCustomHandler
    11 e := initEcho()
    12 e.ServeHTTP(rec, req)
    1 func TestCustomHandler(t *testing.T) {
    2 t.Run("Add userID", func(t *testing.T) {
    3 userID := "12345678"
    4 method := echo.GET
    5 target := "/"
    6 body := strings.NewReader("")
    7 req := httptest.NewRequest(method, target, body)
    8 req.Header.Set("X-USER-ID", userID)
    9 rec := httptest.NewRecorder()
    10
    13
    14 ctx := e.AcquireContext()
    15 defer e.ReleaseContext(ctx)
    16 c := GetContextValues(ctx)
    17 res := rec.Result()
    18 assert.Equal(t, 200, res.StatusCode)
    19 assert.Equal(t, userID, rec.Body.String())
    20 assert.Equal(t, userID, c.UserID)
    21 })
    22 }

    View Slide

  22. initEcho
    1 func initEcho() *echo.Echo {
    2 e := echo.New()
    3 e.Use(setUserIDMiddleware)
    4 e.GET("/", GetUser)
    5 return e
    6 }

    View Slide

  23. TestCustomHandler
    14 ctx := e.AcquireContext()
    15 defer e.ReleaseContext(ctx)
    16 c := GetContextValues(ctx)
    17 res := rec.Result()
    18 assert.Equal(t, 200, res.StatusCode)
    19 assert.Equal(t, userID, rec.Body.String())
    20 assert.Equal(t, userID, c.UserID)
    1 func TestCustomHandler(t *testing.T) {
    2 t.Run("Add userID", func(t *testing.T) {
    3 userID := "12345678"
    4 method := echo.GET
    5 target := "/"
    6 body := strings.NewReader("")
    7 req := httptest.NewRequest(method, target, body)
    8 req.Header.Set("X-USER-ID", userID)
    9 rec := httptest.NewRecorder()
    10
    11 e := initEcho()
    12 e.ServeHTTP(rec, req)
    13
    21 })
    22 }

    View Slide

  24. TestCustomHandler
    14 ctx := e.AcquireContext()
    15 defer e.ReleaseContext(ctx)
    16 c := GetContextValues(ctx)
    17 res := rec.Result()
    1 func TestCustomHandler(t *testing.T) {
    2 t.Run("Add userID", func(t *testing.T) {
    3 userID := "12345678"
    4 method := echo.GET
    5 target := "/"
    6 body := strings.NewReader("")
    7 req := httptest.NewRequest(method, target, body)
    8 req.Header.Set("X-USER-ID", userID)
    9 rec := httptest.NewRecorder()
    10
    11 e := initEcho()
    12 e.ServeHTTP(rec, req)
    13
    18 assert.Equal(t, 200, res.StatusCode)
    19 assert.Equal(t, userID, rec.Body.String())
    20 assert.Equal(t, userID, c.UserID)
    21 })
    22 }

    View Slide

  25. echo AcquireContext/ReleaseContext
    github.com/labstack/echo/v4/echo.go
    1 // AcquireContext returns an empty `Context` instance from the pool.
    2 // You must return the context by calling `ReleaseContext()`.
    3 func (e *Echo) AcquireContext() Context {
    4 return e.pool.Get().(Context)
    5 }
    6
    7 // ReleaseContext returns the `Context` instance back to the pool.
    8 // You must call it after `AcquireContext()`.
    9 func (e *Echo) ReleaseContext(c Context) {
    10 e.pool.Put(c)
    11 }

    View Slide

  26. TestCustomHandler
    17 res := rec.Result()
    18 assert.Equal(t, 200, res.StatusCode)
    19 assert.Equal(t, userID, rec.Body.String())
    20 assert.Equal(t, userID, c.UserID)
    1 func TestCustomHandler(t *testing.T) {
    2 t.Run("Add userID", func(t *testing.T) {
    3 userID := "12345678"
    4 method := echo.GET
    5 target := "/"
    6 body := strings.NewReader("")
    7 req := httptest.NewRequest(method, target, body)
    8 req.Header.Set("X-USER-ID", userID)
    9 rec := httptest.NewRecorder()
    10
    11 e := initEcho()
    12 e.ServeHTTP(rec, req)
    13
    14 ctx := e.AcquireContext()
    15 defer e.ReleaseContext(ctx)
    16 c := GetContextValues(ctx)
    21 })
    22 }

    View Slide

  27. まとめ
    ServeHTTP
    を使うと、複数のMiddleware
    を組み合わせたテストもできる
    公式ドキュメントも情報が足りないので他にスマートな方法があったら知りたい
    サンプルコードはこちら https://github.com/saicologic/echo_custom_middleware_sample

    View Slide

  28. ご清聴ありがとうございました。

    View Slide