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を作る選択肢の一つとして考えておくと良いと思う。