LoginSignup
37

More than 5 years have passed since last update.

gae/g unit testing

Last updated at Posted at 2013-12-05

golangにはgo testというunit test用の機能があります。
testを行うための testing package もあります。

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

そこを解決するために以下のlibraryなどもあったのだけど、
gae 1.8.6でついにgae/gでもunit testができるようになりました!

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集

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37