Slide 1

Slide 1 text

CustomMiddleware のテストを する方法

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

ミドルウェアとテスト レスポンス結果 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

Slide 6

Slide 6 text

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 }

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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 }

Slide 9

Slide 9 text

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 }

Slide 10

Slide 10 text

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 }

Slide 11

Slide 11 text

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 }

Slide 12

Slide 12 text

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 }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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 )

Slide 15

Slide 15 text

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 }

Slide 16

Slide 16 text

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 }

Slide 17

Slide 17 text

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 }

Slide 18

Slide 18 text

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 )

Slide 19

Slide 19 text

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 }

Slide 20

Slide 20 text

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 }

Slide 21

Slide 21 text

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 }

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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 }

Slide 24

Slide 24 text

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 }

Slide 25

Slide 25 text

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 }

Slide 26

Slide 26 text

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 }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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