Go
GoogleAppEngine
GoogleCloudPlatform

testeratorを使ってgae/goのunit testを高速化する

More than 1 year has passed since last update.

testerator はGoogle App Engine for Go(以下gae/go)のUnit Testを高速化するために生まれたライブラリです。

gae/goはサーバのSpinUp速度がJava, Python, PHPに比べて高速なので、最近App Engine Developerたちの間に人気ですが、いかんせんUnitTestの遅さだけは洒落にならないレベルで遅いです。

それを緩和するために生まれたのが、 testerator です。
testerator がやってくれることを理解するためには、まずgae/goのUnitTestがなぜ遅いのかを理解する必要があります。

gae/goのUnitTestが遅い原因は、テスト環境がgae/pythonの環境に間借りしているからです。
例えば、公式のサンプル にある通り、 aetest.NewContext() を利用すると、裏ではgae/pythonにあるgaeのmockのプロセスが立ち上がります。
このプロセスの立ち上がりには3secほどかかります。これがUnitTest開始時に1度だけで済めばよいのですが、環境をクリアするために、UnitTest毎にプロセスの再作成が行われます。
そのため、UnitTestが100個あると、3sec * 100で300secかかります。

UnitTestを実行する度に、珈琲を豆から挽いて淹れることができるレベルです。

import (
        "testing"

        "google.golang.org/appengine/aetest"
)

func TestWithContext1(t *testing.T) {
        ctx, done, err := aetest.NewContext() // ←ここで3sec
        if err != nil {
                t.Fatal(err)
        }
        defer done()

        // Run code and tests requiring the context.Context using ctx.
        // ...
}

func TestWithContext2(t *testing.T) {
        ctx, done, err := aetest.NewContext() // ←ここでも3sec !
        if err != nil {
                t.Fatal(err)
        }
        defer done()

        // Run code and tests requiring the context.Context using ctx.
        // ...
}

testerator はgae/pythonのプロセスの立ち上がりを抑制してくれる

testerator はUnitTestのスピードを上げるために、一度立ち上げてgae/pythonのプロセスを使いまわすようになっています。

import (
    "testing"

    "github.com/favclip/testerator"

    "google.golang.org/appengine/datastore"
)

func TestPut(t *testing.T) {
    _, c, err := testerator.SpinUp() // gae/pythonのインスタンスが無ければ起動、あれば使いまわす
    if err != nil {
        t.Fatal(err.Error())
    }
    defer testerator.SpinDown() // プロセスをシャットダウンせずに、Datastoreなどの内容をクリアする

...
}

func TestGetEmpty(t *testing.T) {
    _, c, err := testerator.SpinUp()
    if err != nil {
        t.Fatal(err.Error())
    }
    defer testerator.SpinDown()

...
}

testerator 利用時の注意点

testerator を使ってUnitTestを高速化するためには、最初にgae/pythonのプロセスを1つ立ち上げる必要があります。
UnitTest毎に testerator.SpinUp() , testerator.SpinDown() を交互に呼ぶだけだと、結局プロセスを毎回作り直してしまうからです。
無難な方法としては、 TestMain(*testing.M) を使って、UnitTest前に testerator.SpinUp() を呼んで、UnitTest後に testerator.SpinDown() を呼びます。

import (
    "fmt"
    "os"
    "testing"

    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/memcache"
    _ "github.com/favclip/testerator/search"

    "github.com/favclip/testerator"
)

func TestMain(m *testing.M) {
    _, _, err := testerator.SpinUp() // 最初の1プロセスを起動!

    if err != nil {
        fmt.Printf(err.Error())
        os.Exit(1)
    }

    status := m.Run() // UnitTest実行!

    err = testerator.SpinDown() // 最初に立ち上げたプロセスを落とす
    if err != nil {
        fmt.Printf(err.Error())
        os.Exit(1)
    }

    os.Exit(status)
}

それでも完璧には早くならなかったよ・・・

testerator はUnitTestの高速化に貢献してくれました。
しかし、たくさんのUnitTestがある時は、爆速にはなりません。
なぜならば、gae/pythonのプロセスをずっと使いまわすると、途中で応答が無くなってしまうので、定期的にgae/pythonのプロセスを再起動しているからです・・・。

何回UnitTestを実行した後にプロセスを再起動するかは ResetThreshold という値が管理されており、デフォルトは15です testarator.go#L52

変更したい場合は testerator.DefaultSetup.ResetThreshold に設定を行います。
もし、なんかよくわからんけど、UnitTestの応答が無くなったら、ちょっと減らしてみると良いかもしれません。

func TestMain(m *testing.M) {
    _, _, err := testerator.SpinUp()
    if err != nil {
        fmt.Printf(err.Error())
        os.Exit(1)
    }
    testerator.DefaultSetup.ResetThreshold = 12

    status := m.Run()

    err = testerator.SpinDown()
    if err != nil {
        fmt.Printf(err.Error())
        os.Exit(1)
    }

    os.Exit(status)
}

さいごに

ここまで読んだあなたはきっと"これって README.md に書いてあったほうがいいんじゃない!?"って思ったことでしょう。
僕もそう思います。
...がんばって、英語書いてpull req送っておきますね。