Golang Echoでのテスト駆動のAPI開発

https://qiita.com/kiyc/items/fc65d999f86d4d59354c

の続きです。
Echoで簡単なAPI、CRUDのCRを作ってみます(DB保存までしませんが)。

https://echo.labstack.com/guide/routing
https://echo.labstack.com/guide/request
https://echo.labstack.com/guide/response
https://echo.labstack.com/guide/testing

# Golangのテスト

まずはその前にHTTPのテストが出来るようにします。

```server.go 
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    router := NewRouter()

    router.Logger.Fatal(router.Start(":8080"))
}

func NewRouter() *echo.Echo {
    e := echo.New()

    e.GET("/hello", helloHandler)

    return e
}

func helloHandler(c echo.Context) error {
    return c.String(http.StatusOK, "Hello")
}
```

```server_test.go 
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
    "github.com/stretchr/testify/assert"
)

func TestHelloHandler(t *testing.T) {
    router := NewRouter()

    req := httptest.NewRequest("GET", "/hello", nil)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.Equal(t, "Hello, Echo World!!", rec.Body.String())
}
```

```sh
$ docker-compose exec app go test -v
# runtime/cgo
exec: "gcc": executable file not found in $PATH
FAIL	_/go/src [build failed]
```

なんかエラー。
環境変数の変更が必要らしい。

```dockerfile 
FROM golang:1.13.6-alpine

ENV CGO_ENABLED=0

WORKDIR /go/src

COPY ./src /go/src

RUN apk update && apk add git
```

```sh
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec app go test -v
=== RUN   TestHelloHandler
--- FAIL: TestHelloHandler (0.00s)
    server_test.go:20: 
        	Error Trace:	server_test.go:20
        	Error:      	Not equal: 
        	            	expected: "Hello, Echo World!!"
        	            	actual  : "Hello"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1 +1 @@
        	            	-Hello, Echo World!!
        	            	+Hello
        	Test:       	TestHelloHandler
FAIL
exit status 1
FAIL	_/go/src	0.004s
```

もちろんアサーションはエラーになりました。
なので`server.go`を修正します。

```server.go 
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    router := NewRouter()

    router.Logger.Fatal(router.Start(":8080"))
}

func NewRouter() *echo.Echo {
    e := echo.New()

    e.GET("/hello", helloHandler)

    return e
}

func helloHandler(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, Echo World!!")
}
```

```sh
$ docker-compose exec app go test -v
=== RUN   TestHelloHandler
--- PASS: TestHelloHandler (0.00s)
PASS
ok  	_/go/src	0.004s
```

成功しました:thumbsup:

# API実装

本題です。
APIのテストを書きます。

```server_test.go 
package main

import (
    "net/url"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
    "github.com/labstack/echo"
    "github.com/stretchr/testify/assert"
)

func TestUserIndexHandler(t *testing.T) {
    router := NewRouter()

    req := httptest.NewRequest(http.MethodGet, "/users", nil)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `[{"name": "Taro", "email": "taro@example.com"}, {"name": "Jiro", "email": "jiro@example.com"}]`, rec.Body.String())
}

func TestUserShowHandler(t *testing.T) {
    router := NewRouter()

    req := httptest.NewRequest(http.MethodGet, "/users/jiro", nil)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `{"name": "Jiro", "email": "jiro@example.com"}`, rec.Body.String())
}

func TestUserCreateHandler(t *testing.T) {
    router := NewRouter()

    form := make(url.Values)
    form.Set("name", "Saburo")
    form.Set("email", "saburo@example.com")
    req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(form.Encode()))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
    rec := httptest.NewRecorder()

    router.ServeHTTP(rec, req)

    assert.Equal(t, http.StatusCreated, rec.Code)
    assert.JSONEq(t, `{"name": "Saburo", "email": "saburo@example.com"}`, rec.Body.String())
}
```

```sh
$ docker-compose exec app go test -v
=== RUN   TestUserIndexHandler
--- FAIL: TestUserIndexHandler (0.00s)
    server_test.go:21: 
        	Error Trace:	server_test.go:21
        	Error:      	Not equal: 
        	            	expected: 200
        	            	actual  : 404
        	Test:       	TestUserIndexHandler
    server_test.go:22: 
        	Error Trace:	server_test.go:22
        	Error:      	Not equal: 
        	            	expected: []interface {}([]interface {}{map[string]interface {}{"email":"taro@example.com", "name":"Taro"}, map[string]interface {}{"email":"jiro@example.com", "name":"Jiro"}})
        	            	actual  : map[string]interface {}(map[string]interface {}{"message":"Not Found"})
        	Test:       	TestUserIndexHandler
=== RUN   TestUserShowHandler
--- FAIL: TestUserShowHandler (0.00s)
    server_test.go:33: 
        	Error Trace:	server_test.go:33
        	Error:      	Not equal: 
        	            	expected: 200
        	            	actual  : 404
        	Test:       	TestUserShowHandler
    server_test.go:34: 
        	Error Trace:	server_test.go:34
        	Error:      	Not equal: 
        	            	expected: map[string]interface {}{"email":"jiro@example.com", "name":"Jiro"}
        	            	actual  : map[string]interface {}{"message":"Not Found"}
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1,4 +1,3 @@
        	            	-(map[string]interface {}) (len=2) {
        	            	- (string) (len=5) "email": (string) (len=16) "jiro@example.com",
        	            	- (string) (len=4) "name": (string) (len=4) "Jiro"
        	            	+(map[string]interface {}) (len=1) {
        	            	+ (string) (len=7) "message": (string) (len=9) "Not Found"
        	            	 }
        	Test:       	TestUserShowHandler
=== RUN   TestUserCreateHandler
--- FAIL: TestUserCreateHandler (0.00s)
    server_test.go:49: 
        	Error Trace:	server_test.go:49
        	Error:      	Not equal: 
        	            	expected: 201
        	            	actual  : 404
        	Test:       	TestUserCreateHandler
    server_test.go:50: 
        	Error Trace:	server_test.go:50
        	Error:      	Not equal: 
        	            	expected: map[string]interface {}{"email":"saburo@example.com", "name":"Saburo"}
        	            	actual  : map[string]interface {}{"message":"Not Found"}
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -1,4 +1,3 @@
        	            	-(map[string]interface {}) (len=2) {
        	            	- (string) (len=5) "email": (string) (len=18) "saburo@example.com",
        	            	- (string) (len=4) "name": (string) (len=6) "Saburo"
        	            	+(map[string]interface {}) (len=1) {
        	            	+ (string) (len=7) "message": (string) (len=9) "Not Found"
        	            	 }
        	Test:       	TestUserCreateHandler
FAIL
exit status 1
FAIL	_/go/src	0.006s
```

当たり前ですが全部404でエラーになります。
次に`server.go`にAPIを追加します。

```server.go
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    router := NewRouter()

    router.Logger.Fatal(router.Start(":8080"))
}

func NewRouter() *echo.Echo {
    e := echo.New()

    e.GET("/users", userIndexHandler)
    e.GET("/users/:name", userShowHandler)
    e.POST("/users", userCreateHandler)

    return e
}

type User struct {
    Name  string `json:"name" form:"name"`
    Email string `json:"email" form:"email"`
}

type Users []User

func userIndexHandler(c echo.Context) error {
    var users Users

    users = append(users, User {
        Name:  "Taro",
        Email: "taro@example.com",
    })
    users = append(users, User {
        Name:  "Jiro",
        Email: "jiro@example.com",
    })

    return c.JSON(http.StatusOK, users)
}

func userShowHandler(c echo.Context) error {
    var user User
    name := c.Param("name")

    if name == "taro" {
        user = User {
            Name:  "Taro",
            Email: "taro@example.com",
        }
    } else if name == "jiro" {
        user = User {
            Name:  "Jiro",
            Email: "jiro@example.com",
        }
    }

    return c.JSON(http.StatusOK, user)
}

func userCreateHandler(c echo.Context) (err error) {
    user := new(User)

    if err = c.Bind(user); err != nil {
        return
    }

    return c.JSON(http.StatusCreated, user)
}
```

```sh
$ docker-compose exec app go test -v
=== RUN   TestUserIndexHandler
--- PASS: TestUserIndexHandler (0.00s)
=== RUN   TestUserShowHandler
--- PASS: TestUserShowHandler (0.00s)
=== RUN   TestUserCreateHandler
--- PASS: TestUserCreateHandler (0.00s)
PASS
ok  	_/go/src	0.006s
```

テスト成功しました。テストがシンプルすぎるのは見逃してください。
`c.Bind(user)`が新鮮に感じます。
普段はPHPを書いていて、そこまで型に厳密ではないので、Golangの型は厳しくも安心感があります。
ただファイルが分かれているけど、`server_test.go`の中で`server.go`の`NewRouter()`を呼べるというのは、`server_test.go`のテスト対象が`server.go`だから？ということでしょうか。
まだまだGolangの道は奥が深そうです。。

# 参考URL

https://qiita.com/theoden9014/items/ac8763381758148e8ce5
https://qiita.com/JpnLavender/items/21b4574a7513472903ea