gae/g unit testing

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ができるようになりました！

https://github.com/najeira/testbed

# [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です。
とても小さなサンプルソースは以下です。

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

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

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

```go: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集

* [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についての解説