前回の続きです。
せっかくオレオレWAFを作ったので、ユニットテストと、go-yaml
を使った環境設定を行いたいと思います。
go-yaml
設定ファイルはyamlが扱いやすいので、go-yamlを使います。
$ go get gopkg.in/yaml.v2
設定用yamlはこのような感じをイメージ。
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
前回のmain.go
を、go-yamlを使って任意の接続先を選択できるように書き換えます。
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して環境変数を設定します。
$ export GOJIENV=development
起動してみます。
$ 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
を使用します。
$ go get github.com/stretchr/testify/assert
net/http/httptest
goji
でnet/http/httptest
でユニットテストをまとめられた方がいるので、こちらを参考にさせていただきました。
http://qiita.com/r_rudi/items/727fb85030e91101801d
httptest
でモックサーバを立てる場合、httptest.NewServer()
にhttpマルチプレクサを渡す必要があります。
前回はWEBプレクサをmain()
内に作成していましたが、httptest
に引数として渡す必要があるため、ルーティングの部分をrooter()
に分離します。
ついでにデータベースコネクトをinit()
からconnect()
に変更してmain()
から呼び出すように変更してます。
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
を使う必要があります。
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
コマンドで行います。
$ echo -n "user:user" | base64
dXNlcjp1c2Vy
テストを実行します。
$ 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()
を書き換えます。
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
}
再度テストを実行します。
$ go test main.go user_controller.go user_nethttp_test.go
ok command-line-arguments 0.325s
今度はちゃんと通りました。
ひと通りCRUDのテストを書きます。
http.Get()
とhttp.Set()
が使えないとつらみが・・・
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)
}
実行します。
$ go test main.go user_controller.go user_nethttp_test.go
ok command-line-arguments 0.346s
無事全パターン通りました。
testflight
net/http/httptest
だけでテストは可能なんですが、接続の周りをうまい具合にラッピングしてくれるtestflight
というパッケージがあるので、それを使うと下記のように書き換えることができます。
testflightを入れる。
$ go get github.com/drewolson/testflight
テストを書き換える。
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)
}
実行します。
$ 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
#まとめ
- テストを書くことでgo言語の仕様とか理解が深まるのでテストはやっぱり大事です。
- go言語はテストフレームワークを内包しているので、テストしやすいところがとても良いと思いました。
- 個人的な感想として、WAFやるだけならRailsなどのフレームワークを使ったほうが良いと思います。Go言語は処理速度を求めるエンドポイントとか、そういった部分に向いてそうです。
- 次はgorutineとかChannelとか練習したい。