GAE/GoとGojiの組み合わせでテストを書く

More than 3 years have passed since last update.

これまで、MartiniとかBeegoとかでGoogle App Engine/Goのアプリケーションを作ってきましたが、Goでのテストを書けていませんでした。CasperJSのe2eテストでごまかしていましたが、今回Gojiでアプリケーションを作るにあたってちゃんとテストまわりを整備しました。


Goji

Go Web Frameworks 比較で紹介されていますが、かなり薄めの実装です。Goだとおそらくそういうのがスタンダードですね。実質的に、ルーティングの部分を楽にかけるぐらいですね。以下のようにREST APIを生やす時に便利です。



package main

import (
"net/http"

"github.com/zenazn/goji"
)

func init() {
http.Handle("/", goji.DefaultMux)

goji.Get("/", indexHandler)
goji.Get("/api/v1/spots", spotHandler)
goji.Get("/api/v1/spots/:spotCode", spotGetHandler)
goji.Get("/edit/", indexHandler)
goji.Get("/edit/v1/spots", spotHandler)
goji.Get("/edit/v1/spots/:spotCode", spotGetHandler)
goji.Post("/edit/v1/spots", spotCreateHandler)
goji.Patch("/edit/v1/spots/:spotCode", spotUpdateHandler)
}


GAE/Goのテスト

以下、テストの一部を書いてます。テストの全体はgithubで公開してるのでそちらを参照してください。



func TestCreateSpot(t *testing.T) {
opt := aetest.Options{AppID: "t2jp-2015", StronglyConsistentDatastore: true}
inst, err := aetest.NewInstance(&opt)
defer inst.Close()
input, err := json.Marshal(Spot{SpotName: "foo", Body: "bar"})
req, err := inst.NewRequest("POST", "/edit/v1/spots", bytes.NewBuffer(input))
if err != nil {
t.Fatalf("Failed to create req: %v", err)
}
loginUser := user.User{Email: "hoge@gmail.com", Admin: false, ID: "111111"}
aetest.Login(&loginUser, req)
ctx := appengine.NewContext(req)
res := httptest.NewRecorder()
c := web.C{}
spotCreateHandler(c, res, req)
if res.Code != http.StatusCreated {
t.Fatalf("Fail to request spots create, status code: %v", res.Code)
}
spots := []Spot{}
_, err = datastore.NewQuery("Spot").Order("-UpdatedAt").GetAll(ctx, &spots)
for i := 0; i < len(spots); i++ {
t.Logf("SpotCode:%v", spots[i].SpotCode)
t.Logf("SpotName:%v", spots[i].SpotName)
}
if spots[0].SpotName != "foo" {
t.Fatalf("not expected value! :%v", spots[0].SpotName)
}

}

この内容について順に解説していきます。


StronglyConsistentDatastore

下は、テスト環境の設定です。StronglyConsistentDatastoreというのを有効すると、データストアへの反映がすぐにされるようになります。デフォルトだと実際のデータストアと同じようにデータの反映にややタイムラグがある仕様となっています。REST APIで書き込んだ値をまたすぐに取得して値をチェックするようなテストの際には、こうしておかないとデータが反映されずにテストが失敗してしまいます。



opt := aetest.Options{AppID: "t2jp-2015", StronglyConsistentDatastore: true}


InstanceはCloseする

開発時に goapp test を繰り返してて、気がついたら、かなりの数のpythonプロセスが上がってました。MacのCPU使用率異常に高いなあと思ってある時、プロセス確認したら、以下のような感じでした。

ps aux | grep python

suzukiyosuke 10586 13.0 0.4 2541932 66300 s000 S 3:20PM 2:25.36 /usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python /Users/suzukiyosuke/go_appengine/dev_appserver.py --port=0 --api_port=0 --admin_port=0 --skip_sdk_update_check=true --clear_datastore=true --clear_search_indexes=true --datastore_path /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest782315070/datastore --datastore_consistency_policy=consistent /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest782315070
suzukiyosuke 10596 12.4 0.4 2541932 66264 s000 S 3:20PM 2:26.28 /usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python /Users/suzukiyosuke/go_appengine/dev_appserver.py --port=0 --api_port=0 --admin_port=0 --skip_sdk_update_check=true --clear_datastore=true --clear_search_indexes=true --datastore_path /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest691799584/datastore --datastore_consistency_policy=consistent /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest691799584
suzukiyosuke 12095 12.3 0.4 2541932 66156 s003 S 3:52PM 0:01.56 /usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python /Users/suzukiyosuke/go_appengine/dev_appserver.py --port=0 --api_port=0 --admin_port=0 --skip_sdk_update_check=true --clear_datastore=true --clear_search_indexes=true --datastore_path /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest330937532/datastore --datastore_consistency_policy=consistent /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest330937532
suzukiyosuke 10587 11.0 0.4 2541932 65988 s000 S 3:20PM 2:26.25 /usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python /Users/suzukiyosuke/go_appengine/dev_appserver.py --port=0 --api_port=0 --admin_port=0 --skip_sdk_update_check=true --clear_datastore=true --clear_search_indexes=true --datastore_path /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest669790853/datastore --datastore_consistency_policy=consistent /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest669790853
suzukiyosuke 12094 3.0 0.4 2541932 65672 s003 S 3:52PM 0:01.58 /usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python /Users/suzukiyosuke/go_appengine/dev_appserver.py --port=0 --api_port=0 --admin_port=0 --skip_sdk_update_check=true --clear_datastore=true --clear_search_indexes=true --datastore_path /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest105125009/datastore --datastore_consistency_policy=consistent /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest105125009
suzukiyosuke 12104 0.8 0.4 2540908 65164 s003 R 3:52PM 0:01.41 /usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python /Users/suzukiyosuke/go_appengine/dev_appserver.py --port=0 --api_port=0 --admin_port=0 --skip_sdk_update_check=true --clear_datastore=true --clear_search_indexes=true --datastore_path /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest152841963/datastore --datastore_consistency_policy=consistent /var/folders/bm/ftlrwxys521d062ww9f5p1qr0000gn/T/appengine-aetest152841963

ちゃんとCloseしないいけなかったのですね。以下のようにdefer でCloseしておけば、ちゃんとtest後にプロセスが消えているようでした。



inst, err := aetest.NewInstance(&opt)
defer inst.Close()


Loginユーザーとしてテストする

GAEの便利なところとして、Googleアカウントでのログイン制御が簡単にできるところです。それのテストができます。



loginUser := user.User{Email: "hoge@gmail.com", Admin: false, ID: "111111"}
aetest.Login(&loginUser, req)


データをPOSTする

普通によくあるテストですが、テストのinstanceを作成したら、それを使ってPOSTリクエストを作って、REST APIのテストをします。



input, err := json.Marshal(Spot{SpotName: "foo", Body: "bar"})
req, err := inst.NewRequest("POST", "/edit/v1/spots", bytes.NewBuffer(input))
if err != nil {
t.Fatalf("Failed to create req: %v", err)
}


データストアの書き込みを確認

REST APIでデータを書き込みをしたら、それが実際に書き込みされているか、確認してみます。



spots := []Spot{}
_, err = datastore.NewQuery("Spot").Order("-UpdatedAt").GetAll(ctx, &spots)
for i := 0; i < len(spots); i++ {
t.Logf("SpotCode:%v", spots[i].SpotCode)
t.Logf("SpotName:%v", spots[i].SpotName)
}
if spots[0].SpotName != "foo" {
t.Fatalf("not expected value! :%v", spots[0].SpotName)
}

これで一通りテストに使いやすそうな機能は紹介しました。


CIの設定

テストは手元でも実行しますが、やはりCIサービスで自動実行されると便利ですよね。最新のGAE SDKを取得するスクリプトを作っていますのでよろしければそちらも利用してください。

CircleCIの設定は以下のとおりです。今回はWebdriver.IOでのe2eテストの設定も入れています。また masterブランチとdeployment/productionへのマージがあった場合は、デプロイも走るようにしています。


machine:
timezone:
Asia/Tokyo

dependencies:
pre:
- python getlatestsdk.py
- unzip -q -d $HOME google_appengine.zip
- npm install -g webdriverio
- npm install -g webdriver-manager
- curl -O http://selenium-release.storage.googleapis.com/2.47/selenium-server-standalone-2.47.0.jar
override:
- echo $HOME

test:
pre:
- java -jar selenium-server-standalone-2.47.0.jar:
background: true
- sleep 5
- webdriver-manager start:
background: true
- sleep 5
- $HOME/go_appengine/goapp serve:
background: true
- sleep 5
override:
- $HOME/go_appengine/goapp test
- wdio wdio.conf.js

deployment:
development:
branch: master
commands:
- $HOME/go_appengine/appcfg.py --oauth2_refresh_token=$APPENGINE_TOKEN update . --version=dev
production:
branch: deployment/production
commands:
- $HOME/go_appengine/appcfg.py --oauth2_refresh_token=$APPENGINE_TOKEN update . --version=production

以上、具体的なコードをもとにテストの仕方を紹介しました。コード群全体は以下のレポジトリで公開していますので、御覧ください。

https://github.com/yosukesuzuki/t2tapp