前回は、Controllerとそのテストについて実装を行いました。本日も引き続き、実装を進めていきます。
<目次>
前回
Error Structの作成
今回は、APIの例外処理を中心に実装を進めていきます。
最初に例外を格納できるStructを作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
mkdir -p api/utils/errors touch api/utils/errors/api_errors.go tree api api ├── README.md ├── app │ ├── application.go │ └── url_mappings.go ├── controllers │ ├── ping_controller.go │ └── products │ ├── products_controller.go │ └── products_controller_test.go ├── domain │ └── products │ └── product.go ├── main.go ├── services │ └── products_service.go └── utils └── errors └── api_errors.go |
api_errors.goには、以下のStructを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// api_errors.go package errors type ApiErr struct { Message string `json:"message"` Status int `json:"status"` Error string `json:"error"` } // Meaning Example { "message": "product 1 not found", "status" : 404, "error" : "not_found" } |
products_controller.goのCreateProduct関数を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// products_controller.go package products import ( "net/http" "github.com/gin-gonic/gin" "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/utils/errors" ) // CreateProduct - Create product func CreateProduct(c *gin.Context) { var product products.Product if err := c.ShouldBindJSON(&product); err != nil { apiErr := errors.ApiErr{ Message: "invalid json body", Status: http.StatusBadRequest, Error: "bad_request", } c.JSON(apiErr.Status, apiErr) return } c.JSON(http.StatusCreated, product) } |
コンテナを立ち上げて、Talend API Tester上から以下のリクエストを送ってみましょう。
1 2 3 4 5 6 |
{ "id": "1", "name": "coca cola", "detail": "The coca cola is very very delicious drink", "price": 200 } |
idは、uint型ではないといけませんが、文字列として送ってみます。

1 2 3 4 5 |
{ "message": "invalid json body", "status": 400, "error": "bad_request" } |
期待通りのエラーを返しました。ちなみに、CreateProduct関数にて、新たに「ShouldBindJSON関数」を使っています。
1 2 |
if err := c.ShouldBindJSON(&product); err != nil { } |
これは、前回実装した以下の処理を代替することができます。
1 2 3 4 5 6 7 8 9 |
bytes, err := ioutil.ReadAll(c.Request.Body) if err != nil { log.Println(err.Error()) return } if err := json.Unmarshal(bytes, &product); err != nil { log.Println(err.Error()) return } |
エラーユーティリティ関数の実装
先ほど実装したBad Requestはうまくいきましたが、もう少し使い勝手がよくなるようにユーティリティ関数を追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// api_errors.go package errors import "net/http" type ApiErr struct { Message string `json:"message"` Status int `json:"status"` Error string `json:"error"` } func NewBadRequestError(message string) *ApiErr { return &ApiErr{ Message: message, Status: http.StatusBadRequest, Error: "bad_request", } } |
NewBadRequestErrorを追加しました。products_controller.goも書き換えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// products_controller.go // CreateProduct - Create product func CreateProduct(c *gin.Context) { var product products.Product if err := c.ShouldBindJSON(&product); err != nil { apiErr := errors.NewBadRequestError("invalid json body") c.JSON(apiErr.Status, apiErr) return } c.JSON(http.StatusCreated, product) } |
これで、文字列を渡すだけでBad Requestを生成できるようになりました。
テストコードの実装
最近のトレンドだと、テストコードが必須になってきてます。そのため、テストコードを描く練習をしましょう。
TestCreateProductNoErrorの修正
前回作成したproducts_controller_test.goの「TestCreateProductNoError」を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// products_controller_test.g func TestCreateProductNoError(t *testing.T) { // Arrange --- p := products.Product{ID: 123, Name: "coca cola"} byteProduct, _ := json.Marshal(p) response := httptest.NewRecorder() c, _ := gin.CreateTestContext(response) c.Request, _ = http.NewRequest( http.MethodPost, "/products", bytes.NewBuffer(byteProduct), ) // Act --- CreateProduct(c) // Assert --- var product products.Product err := json.Unmarshal(response.Body.Bytes(), &product) assert.EqualValues(t, http.StatusCreated, response.Code) // 修正 assert.Nil(t, err) fmt.Println(product) assert.EqualValues(t, uint64(123), product.ID) } |
上記では、http.StatusOK(200)をhttp.StatusCreated(201)に修正しました。200や201は、リクエスト後に返却されるHTTPのステータスコードです。
テストを実行します。
1 2 |
$ go test ./... ok github.com/gouser/money-boy/api/controllers/products 0.029s |
Invalid json bodyのテスト
ここからが新規のテストです。
先ほど実装した「NewBadRequestError」エラーをテストしてみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
func TestCreateProductWith404Error(t *testing.T) { // Arrange --- type demiProduct struct { ID string `json:"id"` Name string `json:"name"` } p := demiProduct{ID: "123", Name: "coca cola"} byteProduct, _ := json.Marshal(p) response := httptest.NewRecorder() c, _ := gin.CreateTestContext(response) c.Request, _ = http.NewRequest( http.MethodPost, "/products", bytes.NewBuffer(byteProduct), ) // Act --- CreateProduct(c) // Assert --- var apiErr errors.ApiErr err := json.Unmarshal(response.Body.Bytes(), &apiErr) assert.EqualValues(t, http.StatusBadRequest, response.Code) assert.Nil(t, err) assert.EqualValues(t, "invalid json body", apiErr.Message) assert.EqualValues(t, 400, apiErr.Status) assert.EqualValues(t, "bad_request", apiErr.Error) } |
要領は、TestCreateProductNoErrorとほぼ同じです。
Bad Requestが発生するようにリクエストパラメータのIDを文字列に変えてリクエストを生成し、CreateProductへ渡しています。
そして、戻り値として得られた値が、期待されたエラーかのテストをしています。
1 2 |
$ go test ./... ok github.com/gouser/money-boy/api/controllers/products 0.033s |
リファクタリング
テストコード内の重複部分を削除しようと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
// products_controller_test.go package products import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/utils/errors" "github.com/stretchr/testify/assert" ) func requestHandler(p interface{}) (*gin.Context, *httptest.ResponseRecorder) { response := httptest.NewRecorder() byteProduct, _ := json.Marshal(p) c, _ := gin.CreateTestContext(response) c.Request, _ = http.NewRequest( http.MethodPost, "/products", bytes.NewBuffer(byteProduct), ) return c, response } func TestCreateProductNoError(t *testing.T) { // Arrange --- p := products.Product{ID: 123, Name: "coca cola"} c, response := requestHandler(p) // Act --- CreateProduct(c) // Assert --- var product products.Product err := json.Unmarshal(response.Body.Bytes(), &product) assert.EqualValues(t, http.StatusCreated, response.Code) assert.Nil(t, err) fmt.Println(product) assert.EqualValues(t, uint64(123), product.ID) } func TestCreateProductWith404Error(t *testing.T) { // Arrange --- type demiProduct struct { ID string `json:"id"` Name string `json:"name"` } p := demiProduct{ID: "123", Name: "coca cola"} c, response := requestHandler(p) // Act --- CreateProduct(c) // Assert --- var apiErr errors.ApiErr err := json.Unmarshal(response.Body.Bytes(), &apiErr) assert.EqualValues(t, http.StatusBadRequest, response.Code) assert.Nil(t, err) assert.EqualValues(t, "invalid json body", apiErr.Message) assert.EqualValues(t, 400, apiErr.Status) assert.EqualValues(t, "bad_request", apiErr.Error) } |
だいぶスッキリしましたね。
テストを実行します。
1 2 |
$ go test ./... ok github.com/gouser/money-boy/api/controllers/products 0.029s |
次回
次回も引き続き、APIの実装を進めていきたいと思います。
コメントを残す
コメントを投稿するにはログインしてください。