GolangでAPIとテストを書く(echo/dbr/glide/goose/mock)
golang以下の記事を参考にして簡単なAPIとそのテストを書いてみた。コードはここ。
Go言語でTestableなWebアプリケーションを目指して|サイバーエージェント 公式エンジニアブログ
使った主なライブラリ・ツール
echo
webフレームワーク。速いらしい。
$ go get -u github.com/labstack/echo
func main() {
conn, err := dbr.Open("mysql", "root:@tcp(localhost:3306)/mboard", nil)
if err != nil {
panic(err)
}
conn.SetMaxIdleConns(200)
conn.SetMaxOpenConns(200)
e := echo.New()
// middlewares
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{echo.GET, echo.PUT, echo.POST, echo.DELETE},
}))
// endpoints
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
e.GET("/messages", func(c echo.Context) error {
return handler.NewMessageWithSession(conn.NewSession(nil)).GetMessages(c)
})
e.POST("/messages", func(c echo.Context) error {
return handler.NewMessageWithSession(conn.NewSession(nil)).CreateMessage(c)
})
std := standard.New(":1323")
std.SetHandler(e)
gracehttp.Serve(std.Server)
}
dbr
database/sql を強化したもの。
$ go get -u github.com/gocraft/dbr
res, err := t.sess.InsertInto("message").Columns("content").Record(model.Message{Content: content}).Exec()
glide
パッケージ管理ツール。
$ brew install glide
$ glide create
で glide.yaml が作られる。glide get で glide.yaml に追加していったり、glide install でvendor下にインストールしたりする。
goose
DBマイグレーションツール。
db/dbconf.yml に以下のようなyamlファイルを置いて
development:
driver: mymysql
open: tcp:localhost:3306*bbs_go/root/
goose create でマイグレーションファイルを作成する。これはgoかsqlで書ける。
$ go get bitbucket.org/liamstask/goose/cmd/goose
$ goose create CreateMessages sql
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
CREATE TABLE message (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
content TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
DROP TABLE message;
$ goose up
Gooseリポジトリのmerge時にバージョンを上げmigrationボタンをSlackに出す - sambaiz-net
テストの書き方
参考にした記事と同じく、handler, service, daoで構成し、 それぞれのコンストラクタの引数に直下のレイヤーのインタフェースを取ることでDIする。
func NewMessage(tx dao.Tx, messageDao dao.Message) *MessageImpl {
return &MessageImpl{tx: tx, messageDao: messageDao}
}
テスト時にはmockを渡す。 mockgenでmockを生成し、以下のようにして入力と出力を指定することができる。
$ go get github.com/golang/mock/gomock
$ go get github.com/golang/mock/mockgen
$ mockgen -package dao -source message.go -destination message_mock.go
ctl := gomock.NewController(t)
defer ctl.Finish()
message := model.Message{ID: 1, Content: "メッセージ"}
messageDaoMoc := dao.NewMockMessage(ctl)
messageDaoMoc.EXPECT().Create(message.Content).Return(int64(1), nil)
messageDaoMoc.EXPECT().FindById(message.ID).Return(&message, nil)
何も指定しないと1回も呼ばれなかったり、2回以上呼ばれるとエラーになるが、 MinTimes()などで柔軟に書くこともできる。 とはいえmockの内容によっては無理にgomockを使うと真にテストしたいことが埋もれてしまうことがあるので、 あくまでmockを作る選択肢の一つとして考えておくと良いと思う。