Go
GAE
GoDay 6

gae/g unit testing

More than 5 years have passed since last update.

golangにはgo testというunit test用の機能があります。

testを行うための testing package もあります。

しかし、gae/gでは、appengine固有の部分が動かないため利用できませんでした。

そこを解決するために以下のlibraryなどもあったのだけど、

gae 1.8.6でついにgae/gでもunit testができるようになりました!

https://github.com/najeira/testbed


Local Unit Testing for Go

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

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

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のものに置き換えます。


approuter.go

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から受け取った情報を保存する


favorite.go

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の実装を差し替えるとかでも良いかもしれません。


favorite_test.go

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集