前回は、例外処理とそのテストについて実装を行いました。本日も引き続き、実装を進めていきます。
前回
プロジェクト構成
新しく以下のファイルを作成してください。
1 2 3 4 |
touch api/domain/products/product_dao.go touch api/domain/products/product_dao_test.go mv api/domain/products/product.go api/domain/products/product_dto.go touch api/domain/products/product_test.go |
daoは、「Data Access Object」、 dtoは、「Data Transfer Object」の略です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
api ├── README.md ├── app │ ├── application.go │ └── url_mappings.go ├── controllers │ ├── ping_controller.go │ └── products │ ├── products_controller.go │ └── products_controller_test.go ├── domain │ └── products │ ├── product_dao.go │ ├── product_dao_test.go │ ├── product_dto.go │ └── product_test.go ├── main.go ├── services │ └── products_service.go └── utils └── errors └── api_errors.go |
バリデーションの実装
バリデーションの実装をしましょう。
バリデーションとは、ユーザーがインプットした情報が、API側で受け取れる形式になっているかチェックする仕組みのことです。
例えば、ProductのNameが空だった場合、NewBadRequestErrorが発生するようにproduct.goファイルに実装してみます。
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 |
// product.go package products import ( "strings" "time" "github.com/gouser/money-boy/api/utils/errors" ) // Product - defines product info uploaded by user type Product struct { ID uint64 `json:"id"` Name string `json:"name"` Detail string `json:"detail"` Price uint64 `json:"price"` Img []byte `json:"img"` CreatedAt time.Time UpdatedAt time.Time DeletedAt time.Time } // New // Validate - check parameters user inputs func (p *Product) Validate() *errors.ApiErr { p.Name = strings.TrimSpace(strings.ToLower(p.Name)) if p.Name == "" { return errors.NewBadRequestError("invalid product name") } return nil } |
ValidateをProductのメソッドとして実装しました。ProductのNameが設定されていないとNewBadRequestErrorが発生します。
検証のため、product_test.goにテストコードを書いてみましょう。
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 |
// product_test.go package products import ( "testing" "github.com/stretchr/testify/assert" ) func TestProductValiateNoError(t *testing.T) { // Arrange --- p := Product{ID: 123, Name: "coca cola"} // Act --- err := p.Validate() // Assert --- assert.Nil(t, err) } func TestProductValiateBadRequestError(t *testing.T) { // Arrange --- p := Product{ID: 123} // without Name // Act --- err := p.Validate() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, "invalid product name", err.Message) assert.EqualValues(t, 400, err.Status) assert.EqualValues(t, "bad_request", err.Error) } |
テストを実行します。
1 2 |
$ go test ./... ok github.com/gouser/money-boy/api/domain/products 0.101s |
DAOの作成
先述の通り、product_dao.goファイルのdaoは「Data Access Object」であり、DBのアクセス処理を記述します。
簡易メソッドの実装
まずは、商品IDから商品情報を取得するGetと商品を保存するSaveを実装します。
DBへの登録はまだ行いません。その代わりに、Map(productsDB)に保存します。
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 |
// product_dao.go package products import ( "fmt" "github.com/gouser/money-boy/api/utils/errors" ) var ( productsDB = make(map[uint64]*Product) ) // Get - product func (p *Product) Get() *errors.ApiErr { result := productsDB[p.ID] if result == nil { return errors.NewNotFoundError(fmt.Sprintf("product %d not found", p.ID)) } p.ID = result.ID p.Name = result.Name p.Detail = result.Detail p.Price = result.Price p.Img = result.Img p.CreatedAt = result.CreatedAt p.UpdatedAt = result.UpdatedAt p.DeletedAt = result.DeletedAt return nil } // Save - product func (p *Product) Save() *errors.ApiErr { current := productsDB[p.ID] if current != nil { if current.Name == p.Name { return errors.NewBadRequestError(fmt.Sprintf("name %s already registered", p.Name)) } return errors.NewBadRequestError(fmt.Sprintf("product %d already exists", p.ID)) } productsDB[p.ID] = p return nil } |
新しいErrorユーティリティ(NewNotFoundError)が必要となるため、api_errors.goに書き込みましょう。
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 |
// 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", } } func NewNotFoundError(message string) *ApiErr { return &ApiErr{ Message: message, Status: http.StatusNotFound, Error: "not_found", } } |
テストコードの実装
テストコードを書いて、動作確認してみましょう。
まずは、Saveからです。
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
// product_dao_test.go package products import ( "testing" "time" "github.com/stretchr/testify/assert" ) // 正常系テスト func TestProductSaveNoError(t *testing.T) { // Arrange --- p := Product{ ID: 1, Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, CreatedAt: time.Now(), UpdatedAt: time.Now(), DeletedAt: time.Now(), } // Act --- err := p.Save() // Assert --- assert.Nil(t, err) } // 同一名の商品が保存されたらエラーが発生 func TestProductSaveBadRequestErrorWithSameName(t *testing.T) { // Arrange --- p := Product{ ID: 1, Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, CreatedAt: time.Now(), UpdatedAt: time.Now(), DeletedAt: time.Now(), } p.Save() p2 := Product{ ID: 1, Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, CreatedAt: time.Now(), UpdatedAt: time.Now(), DeletedAt: time.Now(), } // Act --- err := p2.Save() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, err.Message, "name coca cola already registered") assert.EqualValues(t, err.Status, 400) assert.EqualValues(t, err.Error, "bad_request") } // 同じIDを保存した場合エラーになる func TestProductSaveBadRequestErrorWithSameID(t *testing.T) { // Arrange --- p := Product{ ID: 1, Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, CreatedAt: time.Now(), UpdatedAt: time.Now(), DeletedAt: time.Now(), } p.Save() p2 := Product{ ID: 1, Name: "orange", Detail: "Wonderful Drink!!!", Price: 100, Img: []byte{1, 2, 3}, CreatedAt: time.Now(), UpdatedAt: time.Now(), DeletedAt: time.Now(), } // Act --- err := p2.Save() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, err.Message, "product 1 already exists") assert.EqualValues(t, err.Status, 400) assert.EqualValues(t, err.Error, "bad_request") } |
テストを実行します。
1 2 |
$ go test ./... ok github.com/gouser/money-boy/api/domain/products 0.022s |
続いて、Getのテストを書きます。
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 |
// product_dao_test.go package products import ( "testing" "time" "github.com/stretchr/testify/assert" ) // 正常系テスト func TestProductGetNoError(t *testing.T) { // Arrange --- p := Product{ ID: 1, Name: "coca cola", Detail: "Wonderful Drink!!!", Price: 120, Img: []byte{1, 2, 3}, CreatedAt: time.Now(), UpdatedAt: time.Now(), DeletedAt: time.Now(), } p.Save() newP := Product{ID: 1} // Act --- result := newP.Get() // Arrange --- assert.Nil(t, result) assert.EqualValues(t, p.Name, newP.Name) assert.EqualValues(t, p.Detail, newP.Detail) assert.EqualValues(t, p.Price, newP.Price) assert.EqualValues(t, p.Img, newP.Img) } // 商品が存在しない場合のテスト func TestProductNotFound(t *testing.T) { // Arrange --- p := Product{ID: 100} // Act --- err := p.Get() // Assert --- assert.NotNil(t, err) assert.EqualValues(t, err.Message, "product 100 not found") assert.EqualValues(t, err.Status, 404) assert.EqualValues(t, err.Error, "not_found") } |
テストを実行します。
1 2 |
$ go test ./... ok github.com/gouser/money-boy/api/domain/products 0.022s |
Serviceの作成
products_service.goには、ビジネスロジックを実装します。
Saveサービス
ここでは、商品の検証、保存を実装しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// products_service.go package services import ( "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/utils/errors" ) // CreateProduct - Service func CreateProduct(product products.Product) (*products.Product, *errors.ApiErr) { if err := product.Validate(); err != nil { return nil, err } if err := product.Save(); err != nil { return nil, err } return &product, nil } |
このサービスをcontrollerから呼び出します。
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 |
// 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/services" "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.NewBadRequestError("invalid json body") c.JSON(apiErr.Status, apiErr) return } newProduct, saveErr := services.CreateProduct(product) if saveErr != nil { c.JSON(saveErr.Status, saveErr) return } c.JSON(http.StatusCreated, newProduct) } |
controllerとビジネスロジックを分離することで、機能追加や改修、バグ調査などがしやすくなると思います。
それでは、動作確認をしてみましょう。コンテナを立ち上げてください。
1 2 3 |
pwd /Users/hoge/go/src/github.com/gouser/money-boy docker-compose up |
Talend API TesterからAPIに対してリクエストを送ります。


動作は問題なさそうですね。
Getサービス
ここでは、指定したIDに紐づく商品データを取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// products_service.go package services import ( "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/utils/errors" ) // GetProduct - Service func GetProduct(productID uint64) (*products.Product, *errors.ApiErr) { p := &products.Product{ID: productID} if err := p.Get(); err != nil { return nil, err } return p, nil } |
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 |
// products_controller.go package products import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gouser/money-boy/api/domain/products" "github.com/gouser/money-boy/api/services" "github.com/gouser/money-boy/api/utils/errors" ) // GetProduct - Get product by product id func GetProduct(c *gin.Context) { productID, productErr := strconv.ParseUint(c.Param("product_id"), 10, 64) if productErr != nil { err := errors.NewBadRequestError("product id should be a number") c.JSON(err.Status, err) return } product, getErr := services.GetProduct(productID) if getErr != nil { c.JSON(getErr.Status, getErr) return } c.JSON(http.StatusOK, product) } |
動作確認をしましょう。Ctrl + c でコンテナを停止後、以下のコマンドで立ち上げてください。
1 |
docker-compose up |
最初に、Saveサービスを呼び出してください。


ここでメモリ内に保持している「id=1」を使って、Getサービスを動かしてみます。

OKですね。
URL「http://localhost:8080/products/1」の「1」がIDです。
これが、数値以外のものを渡すとエラーが発生するようにしています。

テスト
GetProductのテストを書いてみましょう。全部で3つです。
- 正常系テスト
- product_idが数値以外のテスト
- 存在しないproduct_idを渡した時のテスト
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
// 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 getRequestHandler(id string) (*gin.Context, *httptest.ResponseRecorder) { response := httptest.NewRecorder() c, _ := gin.CreateTestContext(response) param := gin.Param{Key: "product_id", Value: id} c.Params = gin.Params{param} c.Request, _ = http.NewRequest( http.MethodGet, "/products/:product_id", nil, ) return c, response } // 正常系 func TestGetProductNoError(t *testing.T) { // Arrange p := products.Product{ID: 1, Name: "coca cola"} c, _ := requestHandler(p) CreateProduct(c) c2, response := getRequestHandler("1") // Act --- GetProduct(c2) // Assert --- var product products.Product err := json.Unmarshal(response.Body.Bytes(), &product) assert.EqualValues(t, http.StatusOK, response.Code) assert.Nil(t, err) assert.EqualValues(t, uint64(1), product.ID) } // 不正なIDのテスト func TestGetProductWithInvalidID(t *testing.T) { // Arrange c, response := getRequestHandler("a") // Act --- GetProduct(c) // Assert --- var apiErr errors.ApiErr json.Unmarshal(response.Body.Bytes(), &apiErr) assert.EqualValues(t, http.StatusBadRequest, response.Code) assert.NotNil(t, apiErr) assert.EqualValues(t, apiErr.Message, "product id should be a number") assert.EqualValues(t, apiErr.Status, 400) assert.EqualValues(t, apiErr.Error, "bad_request") } // 指定したIDのプロダクトが存在しないテスト func TestGetProductWithNoProduct(t *testing.T) { // Arrange --- c, response := getRequestHandler("10000") // Act --- GetProduct(c) // Assert --- var apiErr errors.ApiErr json.Unmarshal(response.Body.Bytes(), &apiErr) assert.EqualValues(t, http.StatusNotFound, response.Code) assert.NotNil(t, apiErr) assert.EqualValues(t, apiErr.Message, "product 10000 not found") assert.EqualValues(t, apiErr.Status, 404) assert.EqualValues(t, apiErr.Error, "not_found") } |
ginのControllerのテストでは、コンテキスト(*gin.Context)を作ることが一番難しかったかもしれません。
コンテキストは、getRequestHandler関数にて作成しました。
テストを実行してみましょう。
1 2 |
$ go test ./... ok github.com/gouser/money-boy/api/controllers/products 0.030s |
OKですね。
次回
次回も引き続き、アプリケーション開発を行なっていきたいと思います。
コメントを残す
コメントを投稿するにはログインしてください。