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

  • 11
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

これまで、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

この投稿は Google Cloud Platform Advent Calendar 201522日目の記事です。