Goで作ったWEBアプリケーションの環境ごとの接続設定とテストをする

[前回](http://qiita.com/masahikoofjoyto/items/b2e6c2cad447e48f91ee)の続きです。
せっかくオレオレWAFを作ったので、ユニットテストと、`go-yaml`を使った環境設定を行いたいと思います。

# go-yaml
設定ファイルはyamlが扱いやすいので、[go-yaml](https://github.com/go-yaml/yaml)を使います。

```shell-session
$ go get gopkg.in/yaml.v2
```

設定用yamlはこのような感じをイメージ。

```yaml:db.yml
driver: mysql
development:
  user: root
  password: ""
  db: gorm
production:
  user: hoge
  password: fuga
  db: gorm
test:
  user: root
  password: ""
  db: gorm_test
```
`go-yaml`のサンプルには予め型が決まったyamlの読み込みの方法が記述されていますが、map[interface{}]interface{}を使うと型を決めずに読み込めるようです。

詳しいやり方は下記にまとまっていますので、このまま使わせていただきます。
[http://qiita.com/yamasaki-masahide/items/d6e406c4c11d5870a1c6](http://qiita.com/yamasaki-masahide/items/d6e406c4c11d5870a1c6)

[前回](http://qiita.com/masahikoofjoyto/items/b2e6c2cad447e48f91ee)の`main.go`を、go-yamlを使って任意の接続先を選択できるように書き換えます。

```go:main.go
package main

import (
    "github.com/zenazn/goji"
    "github.com/zenazn/goji/web"
    "github.com/zenazn/goji/web/middleware"
    _ "github.com/go-sql-driver/mysql"
    "gopkg.in/yaml.v2"
    "io/ioutil"
    "os"
)

var db  gorm.DB

// main()はルーティング情報だけ記述する
func main() { 
    user := web.New()
    goji.Handle("/user/*", user)

    user.Use(middleware.SubRouter)
    user.Get("/index", UserIndex)
    user.Get("/new", UserNew)
    user.Post("/new", UserCreate)
    user.Get("/edit/:id", UserEdit)
    user.Post("/update/:id", UserUpdate)
    user.Get("/delete/:id", UserDelete)

    goji.Serve()
}

// 初期処理のDB接続をyamlから呼び出すように変更
func init(){
    yml, err := ioutil.ReadFile("conf/db.yml")
    if err != nil {
        panic(err)
    }

    t := make(map[interface{}]interface{})

    _ = yaml.Unmarshal([]byte(yml), &t)

    conn := t[os.Getenv("GOJIENV")].(map[interface {}]interface {})
    db, err = gorm.Open("mysql", conn["user"].(string)+conn["password"].(string)+"@/"+conn["db"].(string)+"?charset=utf8&parseTime=True")
    if err != nil {
        panic(err)
    }
}
```

設定ファイルは`conf/db.yml`として、先ほどのdb.ymlを保存しています。
環境変数は何でもいいんですが、今回は`GOJIENV`にしました。

exportして環境変数を設定します。

```shell-session
$ export GOJIENV=development
```

起動してみます。

```shell-session
$ go run main.go user_controller.go
2014/12/09 03:45:42.106781 Starting Goji on [::]:8000
```
無事に起動しました。

これにより、`GOJIENV`を`production`や`test`にすることで、ソースを変更せずに接続先を変更することができるようになりました。
# テスト
次に、ソースの変更を行わず環境を変えることでテストDBが読み込めるようになったので、ユニットテストを行いたいと思います。
モデルのユニットテストは`testing`を使って根気よくやれば特に躓くことなくできそうだったので、今回はコントローラー側のユニットテストに注力します。

httpまわりのテストは`net/http/httptest`を使って行えるので、これを使ってテストを実施します。
## アサーションの追加
標準の`testing`だとアサーションを書くのが大変なので`assert`を使用します。

```shell-session
$ go get github.com/stretchr/testify/assert
```

## net/http/httptest
`goji`で`net/http/httptest`でユニットテストをまとめられた方がいるので、こちらを参考にさせていただきました。
[http://qiita.com/r_rudi/items/727fb85030e91101801d](http://qiita.com/r_rudi/items/727fb85030e91101801d)

`httptest`でモックサーバを立てる場合、`httptest.NewServer()`にhttpマルチプレクサを渡す必要があります。

[前回](http://qiita.com/masahikoofjoyto/items/b2e6c2cad447e48f91ee)はWEBプレクサを`main()`内に作成していましたが、`httptest`に引数として渡す必要があるため、ルーティングの部分を`rooter()`に分離します。
ついでにデータベースコネクトを`init()`から`connect()`に変更して`main()`から呼び出すように変更してます。

```go:main.go
package main

import (
    "github.com/zenazn/goji"
    "github.com/jinzhu/gorm"
    "github.com/zenazn/goji/web"
    "github.com/zenazn/goji/web/middleware"
    _ "github.com/go-sql-driver/mysql"
    "gopkg.in/yaml.v2"
    "io/ioutil"
    "os"
    "net/http"
)

var db  gorm.DB

func main() {
    //main()内でconnectとrooterを呼び出す
    connect() // ← DB接続
    rooter(goji.DefaultMux) // ← Muxの設定
    goji.Serve()
}

//Muxに対するルーティング情報をrooter()としてまとめる
func rooter(m *web.Mux) http.Handler {
    m.Use(SuperSecure)
    user := web.New()
    goji.Handle("/user/*", user)
    user.Use(middleware.SubRouter)
    user.Get("/index", UserIndex)
    user.Get("/new", UserNew)
    user.Post("/new", UserCreate)
    user.Get("/edit/:id", UserEdit)
    user.Post("/update/:id", UserUpdate)
    user.Get("/delete/:id", UserDelete)

    return m
}

//init()から変更
func connect(){
    yml, err := ioutil.ReadFile("conf/db.yml")
    if err != nil {
        panic(err)
    }

    t := make(map[interface{}]interface{})

    _ = yaml.Unmarshal([]byte(yml), &t)

    conn := t[os.Getenv("GOJIENV")].(map[interface {}]interface {})
    db, err = gorm.Open("mysql", conn["user"].(string)+conn["password"].(string)+"@/"+conn["db"].(string)+"?charset=utf8&parseTime=True")
    if err != nil {
        panic(err)
    }
}
```
ここまでできたので、一度テストをしてみます。
今回はベーシック認証でアクセスを制限してますが、goの`http.Get`は接続を簡易化してくれるかわりにカスタムヘッダの設定ができないので、`http.NewRequest`と`http.Client.Do`を使う必要があります。

```go:user_controller_test.go
package main

import (
    "github.com/jinzhu/gorm"
    "github.com/zenazn/goji/web"
    _ "github.com/go-sql-driver/mysql"
    "github.com/stretchr/testify/assert"
    "gopkg.in/yaml.v2"

    "models"
    "net/http"
    "io/ioutil"
    "testing"
    "net/http/httptest"
)

func TestUserIndex(t *testing.T) { 
    m := web.New()
    rooter(m)
    ts := httptest.NewServer(m)
    defer ts.Close()

    req, _ := http.NewRequest("GET", ts.URL + "/user/index", nil)
    // http.NewRequestを生成してベーシック認証用のカスタムヘッダを設定
    req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")

    // http.Clientのインスタンスを作ってDoで接続する
    client := new(http.Client)
    response, _ := client.Do(req)

    assert.Equal(t, 200, response.StatusCode)
}

// テスト用のDB接続はinit()で。
// ここでテストDBのリストアも行います。
func init(){
    yml, _ := ioutil.ReadFile("conf/db.yml")
    t := make(map[interface{}]interface{})

    _ = yaml.Unmarshal([]byte(yml), &t)

    conn := t["test"].(map[interface {}]interface {})
    db, _ = gorm.Open("mysql", conn["user"].(string)+conn["password"].(string)+"@/"+conn["db"].(string)+"?charset=utf8&parseTime=True")
    db.DropTable(&models.User{})
    db.CreateTable(&models.User{})
    User := models.User{Name: "deluser"}
    db.Save(&User)    
}
```

Authorizationヘッダ用のbase64エンコードは`base64`コマンドで行います。

```shell-session
$  echo -n "user:user" | base64
dXNlcjp1c2Vy
```

テストを実行します。

```shell-session
$ go test main.go user_controller.go user_nethttp_test.go
--- FAIL: TestUserIndex (0.00 seconds)
        Location:       user_nethttp_test.go:28
        Error:          Not equal: 200 (expected)
                                != 404 (actual)

FAIL
FAIL    command-line-arguments  0.303s
```
404エラーになってしまいました。。。

どうやら`httptest`に渡しているMux内で`goji`の`subRouter`を設定しても引き継がれないのが原因のようです。
対応方法があるのかもしれないのですが、とりあえず`subRouter`を使わなければ問題ないので、`rooter()`を書き換えます。

```go:rooter()
func rooter(m *web.Mux) http.Handler {
    m.Use(SuperSecure)
    //サブルーターを廃止
    m.Get("/user/index", UserIndex)
    m.Get("/user/new", UserNew)
    m.Post("/user/new", UserCreate)
    m.Get("/user/edit/:id", UserEdit)
    m.Post("/user/update/:id", UserUpdate)
    m.Get("/user/delete/:id", UserDelete)
    
    return m
}
```
再度テストを実行します。

```shell-session
$ go test main.go user_controller.go user_nethttp_test.go
ok      command-line-arguments  0.325s
```
今度はちゃんと通りました。

ひと通りCRUDのテストを書きます。
`http.Get()`と`http.Set()`が使えないとつらみが・・・

```go:user_controller_test.go
package main

import (
    "github.com/jinzhu/gorm"
    "github.com/zenazn/goji/web"
    _ "github.com/go-sql-driver/mysql"
    "github.com/stretchr/testify/assert"
    "gopkg.in/yaml.v2"

    "models"
    "net/http"
    "io/ioutil"
    "net/url"
    "strings"
    "testing"
    "net/http/httptest"
)

func TestUserIndex(t *testing.T) { 
    m := web.New()
    rooter(m)
    ts := httptest.NewServer(m)
    defer ts.Close()

    req, _ := http.NewRequest("GET", ts.URL + "/user/index", nil)
    req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
    client := new(http.Client)
    response, _ := client.Do(req)

    assert.Equal(t, 200, response.StatusCode)
}

func TestUserGet(t *testing.T) { 
    m := web.New()
    rooter(m)
    ts := httptest.NewServer(m)
    defer ts.Close()

    req, _ := http.NewRequest("GET", ts.URL + "/user/edit/1", nil)
    req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
    client := new(http.Client)
    response, _ := client.Do(req)

    assert.Equal(t, 200, response.StatusCode)
}

func TestUserCreate(t *testing.T) {
    count_before := 0
    count_after := 0

    m := web.New()
    rooter(m)
    ts := httptest.NewServer(m)
    defer ts.Close()

    db.Table("users").Count(&count_before)
    values := url.Values{}
    values.Add("Name","testman")

    // POSTのあたりつらい
    req, _ := http.NewRequest("POST", ts.URL + "/user/new",strings.NewReader(values.Encode()))
    req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

    client := new(http.Client)
    response, _ := client.Do(req)

    db.Table("users").Count(&count_after)

    assert.Equal(t, 301, response.StatusCode)
    assert.Equal(t, count_before + 1, count_after)
}

func TestUserCreateError(t *testing.T) {
    count_before := 0
    count_after := 0

    m := web.New()
    rooter(m)
    ts := httptest.NewServer(m)
    defer ts.Close()

    db.Table("users").Count(&count_before)
    values := url.Values{}
    values.Add("Name","エラー")

    req, _ := http.NewRequest("POST", ts.URL + "/user/new",strings.NewReader(values.Encode()))
    req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

    client := new(http.Client)
    response, _ := client.Do(req)

    db.Table("users").Count(&count_after)

    assert.Equal(t, 200, response.StatusCode)
    assert.Equal(t, count_before, count_after)
}

func TestUserDelete(t *testing.T) {
    count_before := 0
    count_after := 0
    Users := [] models.User{}

    m := web.New()
    rooter(m)
    ts := httptest.NewServer(m)
    defer ts.Close()

    db.Find(&Users).Count(&count_before)
    req, _ := http.NewRequest("GET", ts.URL + "/user/delete/1", nil)
    req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
    client := new(http.Client)
    client.Do(req)
    db.Find(&Users).Count(&count_after)

    assert.Equal(t, count_before - 1, count_after)
}

func init(){
    yml, _ := ioutil.ReadFile("conf/db.yml")
    t := make(map[interface{}]interface{})

    _ = yaml.Unmarshal([]byte(yml), &t)

    conn := t["test"].(map[interface {}]interface {})
    db, _ = gorm.Open("mysql", conn["user"].(string)+conn["password"].(string)+"@/"+conn["db"].(string)+"?charset=utf8&parseTime=True")
    db.DropTable(&models.User{})
    db.CreateTable(&models.User{})
    User := models.User{Name: "deluser"}
    db.Save(&User)    
}
```

実行します。

```shell-session
$ go test main.go user_controller.go user_nethttp_test.go
ok      command-line-arguments  0.346s
```
無事全パターン通りました。

# testflight
`net/http/httptest`だけでテストは可能なんですが、接続の周りをうまい具合にラッピングしてくれる`testflight`というパッケージがあるので、それを使うと下記のように書き換えることができます。

testflightを入れる。

```shell-session
$ go get github.com/drewolson/testflight
```

テストを書き換える。

```go:user_controller_testflight_test.go
package main

import (
    "github.com/zenazn/goji"
    "github.com/jinzhu/gorm"
    _ "github.com/go-sql-driver/mysql"
    "github.com/drewolson/testflight"
    "github.com/stretchr/testify/assert"
    "gopkg.in/yaml.v2"

    "models"
    "testing"
    "io/ioutil"
    "net/http"
    "net/url"
    "strings"
)

func TestUserIndex(t *testing.T) {
    testflight.WithServer(rooter(goji.DefaultMux), func(r *testflight.Requester) {
        req, _ := http.NewRequest("GET", "/user/index", nil)
        req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
        response := r.Do(req)
        assert.Equal(t, 200, response.StatusCode)
    })
}

func TestUserGet(t *testing.T) {
    testflight.WithServer(rooter(goji.DefaultMux), func(r *testflight.Requester) {
        req, _ := http.NewRequest("GET", "/user/edit/22", nil)
        req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
        response := r.Do(req)
        assert.Equal(t, 200, response.StatusCode)
    })
}

func TestUserCreate(t *testing.T) {
    testflight.WithServer(rooter(goji.DefaultMux), func(r *testflight.Requester) {
        count_before := 0
        count_after := 0
        db.Table("users").Count(&count_before)

        values := url.Values{}
        values.Add("Name","testman")

        req, _ := http.NewRequest("POST", "/user/new", strings.NewReader(values.Encode()))
        req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
        req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
        response := r.Do(req)

        db.Table("users").Count(&count_after)
        assert.Equal(t, 301, response.StatusCode)
        assert.Equal(t, count_before + 1, count_after)
    })
}

func TestUserCreateError(t *testing.T) {
    testflight.WithServer(rooter(goji.DefaultMux), func(r *testflight.Requester) {
        count_before := 0
        count_after := 0
        db.Table("users").Count(&count_before)

        values := url.Values{}
        values.Add("Name","エラー")

        req, _ := http.NewRequest("POST", "/user/new", strings.NewReader(values.Encode()))
        req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
        req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
        response := r.Do(req)


        db.Table("users").Count(&count_after)
        assert.Equal(t, 200, response.StatusCode)
        assert.Equal(t, count_before, count_after)
    })
}

func TestUserDelete(t *testing.T) {
    testflight.WithServer(rooter(goji.DefaultMux), func(r *testflight.Requester) {
        Users := [] models.User{}
        count_before := 0
        count_after := 0
        db.Find(&Users).Count(&count_before)

        req, _ := http.NewRequest("GET", "/user/delete/1", nil)
        req.Header.Set("Authorization", "Basic dXNlcjp1c2Vy")
        r.Do(req)

        db.Find(&Users).Count(&count_after)
        assert.Equal(t, count_before - 1, count_after)
    })
}

func init(){
    yml, _ := ioutil.ReadFile("conf/db.yml")
    t := make(map[interface{}]interface{})

    _ = yaml.Unmarshal([]byte(yml), &t)

    conn := t["test"].(map[interface {}]interface {})
    db, _ = gorm.Open("mysql", conn["user"].(string)+conn["password"].(string)+"@/"+conn["db"].(string)+"?charset=utf8&parseTime=True")
    db.DropTable(&models.User{})
    db.CreateTable(&models.User{})
    User := models.User{Name: "deluser"}
    db.Save(&User)    
}
```

実行します。

```shell-session
$ go test main.go user_controller.go user_testflight_test.go
ok      command-line-arguments  0.443s
```
通りました。

`net/http/httptest`を使うか`testflight`などのパッケージを使うかはお好みで。

コードはgithubに置きました。
[goji_waf_sample](https://github.com/masahikoofjoytoy/goji_waf_sample)

#まとめ
* テストを書くことでgo言語の仕様とか理解が深まるのでテストはやっぱり大事です。
* go言語はテストフレームワークを内包しているので、テストしやすいところがとても良いと思いました。
* 個人的な感想として、WAFやるだけならRailsなどのフレームワークを使ったほうが良いと思います。Go言語は処理速度を求めるエンドポイントとか、そういった部分に向いてそうです。
* 次はgorutineとかChannelとか練習したい。