golangにはgo testというunit test用の機能があります。
testを行うための [testing] (http://golang.org/pkg/testing/) package もあります。
しかし、gae/gでは、appengine固有の部分が動かないため利用できませんでした。
そこを解決するために以下のlibraryなどもあったのだけど、
gae 1.8.6でついにgae/gでもunit testができるようになりました!
[Local Unit Testing for Go] (https://developers.google.com/appengine/docs/go/tools/localunittesting)
gae/g unit testで重要なのは、以下の3つです。
- goapp test
- appengine/aetest
- testing
この3つを抑えておけば、とりあえずunit testを作り始めることができます。
goapp test
goapp testはgae/gでunit testを実行するためのcommandです。
通常、golangでは、go testでunit testが実行できますが、gae/gにはsdkに入っているgoapp testを利用します。
goappはsdkの直下に入っているので、pathを通しておけば、実行するのが楽ちんです。
[appengine/aetest] (https://code.google.com/p/appengine-go/source/browse/appengine/aetest/)
appengine/aetestは、appengine固有の部分をtestするためのpackageです。
とても小さなサンプルソースは以下です。
import (
"appengine/aetest"
"testing"
)
func TestPost(t *testing.T) {
c, err := aetest.NewContext(nil)
if err != nil {
t.Fatal(err)
}
defer c.Close()
}
appengine contextをaetestのものに差し替えることで、unit testでも動作するようになります。
defer c.Close()で閉じないと、unit testが終わらなくなるので注意です。
Datastoreはaetest.NewContextでまっさらな状態が作られます。
そのため、あんまり何も考えずに作っても、unit testのfunctionごとに、初期化されるはずです。
裏側はpython側の環境を利用しているようです。
golangのlocal環境はpythonを間借りしているものが多いので、こいつも同じような感じということです。
[testing] (http://golang.org/pkg/testing/)
testing packageは、golangでunit testをするために用意されているpackageです。
gae/g固有のものでは無いけど、これがないとunit testを書くのが始まりません。
詳細はdocumentを見ていただければと思いますが、assert機能があるわけではなく、基板となる機能がある感じです。
実際にtestを書いてみたサンプル
ここまでで登場人物の紹介は終わりました。
後は、実際にtestを書いたサンプルを作ってみました。
Production Code
以下がunit testされる側のcodeです。
まずは、approuter.goですが、これはrequestが飛んできた時の入り口です。
requestを受け取って、ちょっとlog出力して、favorite.Process()を呼んでいるだけです。
ここで覚えていて欲しいのは、favorite.Processにappengine.NewContext()で生成した値を渡していることです。
unit testでは、これをappengine/aetestのものに置き換えます。
package api
import (
"appengine"
"net/http"
"process/favorite"
)
func init() {
http.HandleFunc("/", handler)
}
func handler(w http.ResponseWriter, r *http.Request) {
c := appengine.NewContext(r)
c.Infof("request url = %s", r.URL.Path)
c.Infof("http header = %s", r.Header)
switch r.URL.Path {
default:
http.Error(w, "not found.", http.StatusNotFound)
case "/favorite":
favorite.Process(w, r, c)
}
}
favorite.goはrequestを実際に処理しているものです。
他のSampleの使いまわすなので、ちょっと処理が多いけど、大まかにやっているのは2つです。
- GET時 : Datastoreから全データを引っ張ってきて返す
- POST時 : DatastoreにRequestから受け取った情報を保存する
package favorite
import (
"appengine"
"appengine/datastore"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type Favorite struct {
Id string `json:"id" datastore:"-"`
PokemonName string `json:"pokemonName"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Created time.Time `json:"created"`
}
func Process(w http.ResponseWriter, r *http.Request, c appengine.Context) {
val, err := handler(c, r)
if err == nil {
err = json.NewEncoder(w).Encode(val)
}
if err != nil {
c.Errorf("favorite error: %#v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func handler(c appengine.Context, r *http.Request) (interface{}, error) {
switch r.Method {
case "POST":
favorite, err := decodeFavorite(r.Body)
if err != nil {
return nil, err
}
return favorite.save(c)
case "GET":
return getAllFavorites(c)
}
return nil, fmt.Errorf("method not implemented")
}
func (f *Favorite) Key(c appengine.Context) *datastore.Key {
return datastore.NewKey(c, "Favorite", fmt.Sprintf("%s-_-%s", f.Email, f.Nickname), 0, nil)
}
func (f *Favorite) save(c appengine.Context) (*Favorite, error) {
f.Created = time.Now()
k, err := datastore.Put(c, f.Key(c), f)
if err != nil {
return nil, err
}
f.Id = k.StringID()
return f, nil
}
func decodeFavorite(r io.ReadCloser) (*Favorite, error) {
defer r.Close()
var Favorite Favorite
err := json.NewDecoder(r).Decode(&Favorite)
return &Favorite, err
}
func getAllFavorites(c appengine.Context) ([]Favorite, error) {
favos := []Favorite{}
ks, err := datastore.NewQuery("Favorite").Order("Created").GetAll(c, &favos)
if err != nil {
return nil, err
}
for i := 0; i < len(favos); i++ {
favos[i].Id = ks[i].StringID()
}
return favos, nil
}
Test Code
以下がtest codeです。
SampleのためPostしか作ってないですが、やっているのは、PostされたデータがDatastoreに保存されたかどうかの確認です。
aetest.NewContext()で生成した値を渡してやる必要があるため、Production側のfavorite.Processにappengine.NewContext()で生成した値を渡すようにしているわけです。
ただ、unit testのためにそこを修正するのは・・・、ということもあるので、factoryを作って、unit test時にfactoryの実装を差し替えるとかでも良いかもしれません。
package favorite
import (
"../../process/favorite"
"appengine/aetest"
"appengine/datastore"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestPost(t *testing.T) {
c, err := aetest.NewContext(nil)
if err != nil {
t.Fatal(err)
}
defer c.Close()
json := `{"Email":"sinmetal@example.com", "Nickname":"ヴァリトラ"}`
b := strings.NewReader(json)
request, _ := http.NewRequest("POST", "/favorite", b)
response := httptest.NewRecorder()
favorite.Process(response, request, c)
if response.Code != http.StatusOK {
t.Fatalf("Non-expected status code%v:\n\tbody: %v", "200", response.Code)
}
favos := []favorite.Favorite{}
ks, err := datastore.NewQuery("Favorite").Order("Created").GetAll(c, &favos)
if err != nil {
t.Fatal(err)
}
for i := 0; i < len(favos); i++ {
favos[i].Id = ks[i].StringID()
}
if len(favos) < 1 {
t.Fatal(len(favos))
}
if favos[0].Id != "sinmetal@example.com-_-ヴァリトラ" {
t.Fatal(favos[0].Id)
}
}
終わり
gae/gはまだExperimentalだけど、unit testができるようになったことで、
だいぶ、いける!って感じが出てきました。
来年は、gae/gで何かService作って、リリースしたいなーと思っています。
今回のSampleはGithubに置いてます。
ただ、angularjsのsampleの裏として作っているので、他の機能のsourceも一緒においてます。
https://github.com/sinmetal/angularjs-sample
参考情報
golangでunit testをする上で参考になりそうなLink集
-
[Google App Engine 1.8.6 で Go言語の単体テストがサポート] (http://najeira.blogspot.jp/2013/10/google-app-engine-186-go.html)
gae/g unit testサポートの速報 -
[go言語のテスティングフレームワークについて] (http://blog.satotaichi.info/testingframeworks-for-golang)
testingだけじゃ力不足だと感じる場合に検討するlibraryについて -
[GoConvey] (http://smartystreets.github.io/goconvey/)
unit testの結果をブラウザで見れるlibrary
gae/gで動くのかは分からないけど・・・ -
[Golang Cafe #1を開催しました。] (http://takashi-yokoyama.blogspot.jp/)
標準のtestのやり方の解説など -
[testingパッケージのExamplesについて] (http://d.hatena.ne.jp/taknb2nch/20131101/1383285018)
testing packageのExamplesについての解説