GAE/Goでもローカルサーバで本番環境に近い状態でテストしたい!
と、思ったので、色々設定してテストコードを書いたので、メモを残しておこうと思います。
公式のドキュメントにも詳しく書かれていますが、自分用メモということで。
そもそもContextはaetest.NewContext()で作らないと異常終了する
公式のコードを見ればわかりますが、GAE/Goでは、appengine.Contextを使ってAPIを呼び出します。が、Controllerでappengine.NewContext()を呼び出すと、テスト実行時に異常終了してしまうので、実際にコードを書くときは、引数にContextを受け取るようにしなければいけません。
この辺の問題を解決するのは大変だと思いますが、とりあえず、私のパターンをメモしておきます。
package controller
import (
"appengine"
"fmt"
"net/http"
)
func init() {
http.HandleFunc("/", errorController(process))
}
func errorController(f func(w http.ResponseWriter, r *http.Request) *AppError) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := process(w, r)
if err != nil {
http.Error(w, err.Err.Error(), err.Status)
}
}
}
func process(w http.ResponseWriter, r *http.Request) *AppError {
c := appengine.NewContext(r)
switch r.URL.Path {
case "/addclass":
return addClass(c, w, r)
case "/getclass":
return getClass(c, w, r)
default:
c.Warningf("Bad Request[%s]", r.URL.Path)
return &AppError{Err: fmt.Errorf("Not Found."), Status: http.StatusNotFound}
}
return nil
}
コントローラへのリクエストを一つのハンドラにまとめてやることにしています。(実際には、もっとエラーハンドリングをちゃんとしないといけないと思いますが、とりあえずはこんな所で。
(GAE/Goの場合は、http.HandleFunc()で定義していないリクエストは全て"/"扱いになるのですが、実際のリクエストURLを取得して呼び出すControllerを振り分けています。
テストコードは各コントローラに対してリクエストを生成して引数で渡して処理させる感じですね。
コントローラの定義はこんな感じ。
func addClass(c appengine.Context, w http.ResponseWriter, r *http.Request) *AppError {
// なんか処理
}
こういうテスト方法だと、Gojiはちょっと使えない。
テスト時にContextを引数に渡して処理させる感じにしようと思うと、Gojiは使いにくかったです。(私が使い方を知らないだけかもしれないが)なので、結局標準のhttpパッケージを使った「オレオレフレームワーク」になってしまいました。
テスト用のリクエストを作る時は、aetest.Instanceを使え!
Golang標準でもnet/httpパッケージにリクエストを作るためのAPIが用意されていますが、GAE/Goの場合は、appengine/aetestパッケージに用意されている、aetest.Instanceを使った方が、より本番環境に近いリクエストが生成されるようです。
(普通にテストをする(入力/出力を確認する)だけなら、net/httpを使っても特に違いが出ることはないと思うのですが…)
標準的な使い方は以下のようにします。
instance, err := aetest.NewInstance(nil)
if err != nil {
t.Fatalf("NewInstance is failed.[%v]", err)
}
defer c.Close()
注意点(その1)
注意しないといけないのが、標準の状態だと、datastore.Query()を使う時に、本番環境と同じようにdatastoreの遅延が発生してしまいます。テストコードでdatastoreの中身を確認する時にkeyがわからない時は、「とりあえず、Queryで全件…」という事があると思います。
毎回、time.Sleep()を使って待つと「スローテスト問題」にすぐにぶつかってしまいますから、引数にaetest.Optionを指定します。
opt := aetest.Options{AppID:"Your AppID", StronglyConsistentDatastore: true}
instance, err := aetest.NewInstance(opt)
if err != nil {
t.Fatalf("NewInstance is failed.[%v]", err)
}
defer c.Close()
aetest.Option構造体のメンバにStronglyConsistentDatastoreがあって、StronglyConsistentDatastoreにtrueを設定しておくと、datastoreの遅延のエミュレートをしなくなります。
注意点(その2)
この注意点はもっとdatastoreの遅延よりも重要です。
aetest.NewInstance()を呼び出して、instanceを受け取ると、instance.Close()を呼び出さないと、goapp testコマンドでバックグラウンドで起動されているPythonプロセスが停止しないという問題にぶち当たります。
(私は知らずにテストをしていてPythonプロセスが増殖していくのでPCが重たくなりました…)
ログイン状態を再現する
GAEでは、Googleのアカウントでログインしているかどうかをアプリケーションから参照する事ができますが、goapp testでテストを動かす時はテストコードにログイン状態を設定しなければいけません。
c, err := aetest.NewContext(opt)
if err != nil {
t.Fatalf("NewContext is failed.[%v]", err)
}
var u user.User
u.Email = "test@example.com"
u.ID = "1"
c.Login(&u)
ログアウトさせる
ログアウトさせたい場合は、Context.Logout()を呼び出して下さい。
今後メモは増えるか?
この記事に追記するかもしれないし、存在を忘れていたら別の記事を書くかもしれません。
Disclaimer
- この記事は個人的なものです。私の雇用者とは全く関係はありません。(一応つけておきます)