24
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

の続きです。
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())
}
$ docker-compose exec app go test -v
# runtime/cgo
exec: "gcc": executable file not found in $PATH
FAIL	_/go/src [build failed]

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

FROM golang:1.13.6-alpine

ENV CGO_ENABLED=0

WORKDIR /go/src

COPY ./src /go/src

RUN apk update && apk add git
$ 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!!")
}
$ 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())
}
$ 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)
}
$ 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.goNewRouter()を呼べるというのは、server_test.goのテスト対象がserver.goだから?ということでしょうか。
まだまだGolangの道は奥が深そうです。。

参考URL

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

24
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?