Go な WebAPI のテスト＆ドキュメントの模索

みなさん Go な WebAPI のテスト＆ドキュメントどうしてますか。僕はまだ数ヶ月程度の Go 経験しかなく模索しながら、まだまだ彷徨っている状態です。そこで現時点での自分なりのやり方をまとめておこうと思います。こうやったほうが良いよ！というアドバイスがあればぜひお願いします :smile: 

サンプルコードはこちらのリポジトリに雑においてあります。

- https://github.com/zaru/go-api-documents-demo

## ツール類

今記事では echo + sqlx な構成を例にしています。

### 使っているもの

- Go 1.9.x
    - labstack/echo
    - jmoiron/sqlx
    - stretchr/testify/assert
- API Blueprint
    - aglio
    - dredd
    - drakov

テストライブラリは [stretchr/testify/assert](https://github.com/stretchr/testify) だけ使っています。モックなどの機能もありますが、単純に `assert` としてしか使ってません。

ドキュメントには API Blueprint を使っています。ツール類は定番のものを採用しています。ドキュメントと実装の乖離が一番の懸念点でしたが、そこはドキュメントをベースに E2E テストをしてくれる `dredd` がある程度は担保してくれています。

### 使わなかったもの

- DATA-DOG/go-sqlmock.v1

当初はモデルのユニットテストで SQL をモックするために使っていましたが、今は使っていません。

## テスト方針

WebAPI をテストするにあたってどこをどうやってテストするかを決めました。

- ハンドラはモックを使ってテストをする
- モデルはテストデータベースを使ってテストする
- API は E2E で JSON のテストをする

大まかに分けるとこの3つに分けます。

### ハンドラのテスト

ハンドラは色んなものを呼ぶのでテストしにくい部分です。ここは Interface を使ってモックにさしかえながらテストをしていきます。

```go
type handler struct {
	UserModel user.UserModelImpl
}

func NewHandler(u user.UserModelImpl) *handler {
	return &handler{u}
}

func (h *handler) GetIndex(c echo.Context) error {
	lists := h.UserModel.FindAll()
	u := &resultLists{
		Users: lists,
	}
	return c.JSON(http.StatusOK, u)
}
```

Userハンドラのコードです。 `handler` 構造体をレシーバとするメソッドを定義します。 `hander` 構造体は `UserModelImpl` というモデルの Interface を持つようにしています。

こうすることでモデルをモックに差し替えることができます。

```go
e := echo.New()

d := db.DBConnect()
h := users.NewHandler(user.NewUserModel(d))

e.GET("/users", h.GetIndex)
```

メインのサーバ部分のコードです。Userハンドラにモデルを注入して生成しています。テスト時には `user.NewUserModel(d)` を `UserModelImpl` Interface を満たしたものに差し替えるイメージです。


```go
type (
	UserModelImpl interface {
	  FindAll() []User
	}

	User struct {
		ID   int    `json:"id" db:"id"`
		Name string `json:"name" db:"name"`
	}

	UserModel struct {
		db *sqlx.DB
	}
)

func NewUserModel(db *sqlx.DB) *UserModel {
	return &UserModel{
		db: db,
	}
}

func (u *UserModel) FindAll() []User {
	users := []User{}
	u.db.Select(&users, "SELECT * FROM users order by id asc")
	return users
}

```

モデルのコードです。ここでは `FindAll() []User` を実装していれば `UserModelImpl` Interface を満たしているということになります。

```go
type (
	UsersModelStub struct{}
)

func (u *UsersModelStub) FindAll() []user.User {
	users := []user.User{}
	users = append(users, user.User{
		ID:   100,
		Name: "foo",
	})
	return users
}

func TestGetIndex(t *testing.T) {
	e := echo.New()
	req := httptest.NewRequest(echo.GET, "/", nil)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)
	c.SetPath("/users")

	u := &UsersModelStub{}
	h := NewHandler(u)

	var userJSON = `{"users":[{"id":100,"name":"foo"}]}`

	if assert.NoError(t, h.GetIndex(c)) {
		assert.Equal(t, http.StatusOK, rec.Code)
		assert.Equal(t, userJSON, rec.Body.String())
	}
}
```

Userハンドラのテストコードです。モデルのモックを作るためにここで `UsersModelStub` という構造体を作り `FindAll() []User` メソッドを定義します。返却する値をテストしたい適当なデータに差し替えます。

そして、テスト時にUserハンドラにモックモデルを注入することで `FindAll()` の挙動を変えられます。Go のテストは基本的にこの Interface を活用しながらモックでやっていくのが良いのかなと思っています。


### モデルのテスト

悩んだのがモデルテストを実際のデータベースを利用するタイプにするか、SQL や DAO をモックにしてテストするかです。最初はユニットテストとして後者のモック形式でやっていましたが、SQL をモックにすると SQL 自体の妥当性や、実データにまつわるテストが曖昧になるなぁと思い、実行コストがかかっても前者の実際のデータベースを利用するタイプにしました。

#### テスト用のデータベースを作る

Go 1.4 から `TestMain` が使えるのでこれを利用してテスト前の初期化処理と後処理を作ります。 `setup()` でテストデータベースの作成から、マイグレーション、テストデータの読み込みを実行し、 `clear()` でデータベースをまるっと削除しています。

説明用に雑にまとめて書いているので実際は分割して共通化をする感じになると思います。

```go
func setup() {
	err := godotenv.Load("../.env.test")
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	exec.Command("mysql", "-h", os.Getenv("DB_HOST"), "-u", "root", "-e", "create database "+os.Getenv("DB_NAME")).Run()
	exec.Command("goose", "-dir", "../migrations", "mysql", "root@("+os.Getenv("DB_HOST")+")/"+os.Getenv("DB_NAME"), "up").Run()
	cmd := exec.Command("mysql", "-h", os.Getenv("DB_HOST"), "-u", "root", os.Getenv("DB_NAME"))
	readFile := "../seed/test.sql"
	input, err := ioutil.ReadFile(readFile)
	if err != nil {
		panic(err)
	}
	stdin, _ := cmd.StdinPipe()
	stdin.Write(input)
	stdin.Close()

	cmd.Run()
}

func clear() {
	exec.Command("mysql", "-h", os.Getenv("DB_HOST"), "-u", "root", "-e", "drop database "+os.Getenv("DB_NAME")).Run()
}

func TestMain(m *testing.M) {
	setup()
	ret := m.Run()
	clear()

	os.Exit(ret)
}
```

テストコードは非常にシンプルですね。

```go
func TestFindAll(t *testing.T) {

	d := db.DBConnect()
	um := NewUserModel(d)

	u1 := User{
		ID:   1,
		Name: "zaru",
		Rank: 1,
	}

	u := um.FindAll()

	expect := []User{}
	expect = append(expect, u1)
	assert.Equal(t, expect, u)
}
```

#### DATA-DOG/go-sqlmock.v1 を使ったテスト

結果的に採用はしませんでしたが、 `DATA-DOG/go-sqlmock.v1` を使ったテストコードも記載しておきます。

```go
import (
	"database/sql"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/jmoiron/sqlx"
	sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1"
)

func MockDB(t *testing.T) (*sql.DB, sqlmock.Sqlmock, *sqlx.DB) {
	mockDB, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("An error '%s' was not expecting", err)
	}

	sqlxDB := sqlx.NewDb(mockDB, "sqlmock")
	return mockDB, mock, sqlxDB
}

func TestFindAll(t *testing.T) {
	mockDB, mock, sqlxDB := test.MockDB(t)
	defer mockDB.Close()

	u1 := User{ID: 1, Name: "foobar"}
	u2 := User{ID: 2, Name: "barbaz"}

	var cols []string = []string{"id", "name"}
	mock.ExpectQuery("SELECT *").WillReturnRows(sqlmock.NewRows(cols).
		AddRow(u1.ID, u1.Name).
		AddRow(u2.ID, u2.Name))

	um := NewUserModel(sqlxDB)
	u := um.FindAll()

	expect := []User{}
	expect = append(expect, u1, u2)
	assert.Equal(t, expect, u)
}
```

これは `SELECT *` が含まれている SQL の結果を全て差し替えるようにしています。他にも正規表現などでマッチングさせたりもできます。


### E2Eテスト

最後に WebAPI の出力を E2E でテストします。

API Blueprint はマークダウンで API の仕様を記述できます。 `Attributes` セクション（MSON）を書くことで自動で JSON Schema に変換してくれます。MSON フルスペックな JSON Schema を表現することはできませんが、最低限レベルのことは書けるので今はこれでやっています。

```md
FORMAT: 1A

# Go API ドキュメント デモ

# Group グループです

## ユーザ [/users]

### 一覧 [GET]

+ Response 200 (application/json; charset=UTF-8)

    + Attributes
      + users (array[User], fixed-type)

## Data Structures

### User
+ id: `100` (number) - User ID
+ name: `zaru` (string) - User name
+ rank: `1` (enum[number]) - User rank
  + 1
  + 2
  + 3
  + Default: 1
```

#### Dredd のフックを使う

モデルのテストと同様にテスト用のデータベースを用意する必要があります。さいわい Dredd にはフックが用意されているのでそれを利用します。フック用の言語は Go, JavaScript, Ruby など[一通り用意](https://dredd.readthedocs.io/en/latest/)されています。

ここでは JavaScript で書いてみます。

`beforeAll()` でデータベースの作成、マイグレーションなどを実行します。そして `afterAll()` でデータベースの削除をします。モデルと同じノリですね。

ハマったのは、Dredd はテスト時にテスト対象のサーバを起動してくれるのですが、テスト終了後にプロセスを `kill` してくれないという現象です。ここでは `lsof` コマンドを使って手動でプロセスを `kill` しています…。

```js
const hooks = require('hooks')
const execSync = require('child_process').execSync

const testDBHost = 'db'
const testDBName = 'sample_test'
const testAppPort = '1323'

hooks.beforeAll(function (transactions, done) {
  hooks.log('before all')

  let result = execSync('mysql -h ' + testDBHost + ' -u root -e "create database ' + testDBName + '"').toString()
  hooks.log('create database: ' + result)

  result = execSync('goose -dir ./migrations mysql "root@(' + testDBHost + ')/' + testDBName + '" up').toString()
  hooks.log('migration: ' + result)

  result = execSync('mysql -h ' + testDBHost + ' -u root ' + testDBName + ' < ./seed/test.sql').toString()
  hooks.log('seed: ' + result)

  done()
})

hooks.beforeEach(function (transaction, done) {
  hooks.log('before each')
  done()
})

hooks.afterEach(function (transaction, done) {
  hooks.log('after each')
  done()
})

hooks.afterAll(function (transactions, done) {
  hooks.log('after all')

  let result = execSync('mysql -h ' + testDBHost + ' -u root -e "drop database ' + testDBName + '"').toString()
  hooks.log('drop database: ' + result)

  result = execSync('kill `lsof -ti tcp:' + testAppPort + '`')
  hooks.log('kill app: ' + result)

  done()
})
```

## まとめ

以上で、ハンドラなどのモックを使ったテスト、実データベースを使ったモデル・E2E テストができるようになりました。また、API Blueprint でドキュメントと実装の乖離をある程度防げるようになりました。ここまで来るのにすごい苦労をしたような気がしますが、それでも Go は書いてて楽しい気がしています。

俺はこうやってるよ〜という意見お待ちしております。ありがとうございました。