APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

testeratorで高速にGAE/Golangのテストをしよう

始めに

先進サービス開発事業部の山岡です。

GAEにはローカルでクラウドの環境をそこそこ再現できるSDKが用意されており、疑似的にDB(Datastore)等が再現され動作確認を行うことができます。実環境へのアップには1,2分はかかり、ちょくちょくアップしながら動作確認をしているとそれだけで結構な時間が費やされてしまうため、手元で迅速に確認できるのはとてもありがたいのです。

単体テストも同様に疑似的なDBと連携しながら行うことができるので、わざわざ自分でDockerで用意したりする必要はありません(Datastoreの場合はそもそもマネージドサービス)。

しかし純正品( "google.golang.org/appengine/aetest" パッケージ)の動作は方々で言われているように極めて緩慢であり、手元の環境で頻繁に単体試験を行うような事は実質的に不可能と言ってよいと思います。

これはテスト1件(Golangでいうところの TestXxx 関数1つ)あたり毎回環境をシミュレートするPythonプロセスのUp/Downがかかるためであり、デスクトップでCore i7等のそれなりに早いCPUを使っているマシンであっても1,2秒はかかってしまいます。当然テストは1つや2つでは済みませんので、それが累計されると非常に大きなオーバーヘッドになってしまいます。

高速化ライブラリ testerator

そこで登場するのが github.com/favclip/testerator です。これは先に挙げたPythonプロセスを使い回すことでオーバーヘッドを削減してくれるライブラリであり、かなりの改善が見込めます。

使い方は簡単でテスト冒頭の環境立ち上げを次のように置き換えればOKです。

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

    // do something test

但し、何も考えずこれに置き換えるだけだと結局毎回PythonプロセスのUp/Downが走ってしまいます。

testeratorは内部で何度 SpinUp() を呼ばれたのかカウントを取っており 0 -> 1 のときだけ実際に環境を立ち上げそれ以外の場合についてはカウンターを +1 するだけで、プロセスの立ち上げは行いません。

また SpinDown() が呼ばれた場合はカウンターを -1 し更にDatastoreやMemcacheの内容を消去します。そしてこちらは 0 になる時にだけプロセスを落とします(当初この動作を知らず func flushDatastore(ctx context.Context) みたいなのを作っていました……)。

こういった振る舞いをするため、テストの頭でマスターとして SpinUp() を呼ぶ必要があります。順当に行くのであれば TestMain を使うのが良いでしょう。次のように書くことでテスト全体の前処理・後処理を行うことができます。

func TestMain(m *testing.M) {
    _, _, err := testerator.SpinUp()
    if err != nil {
        log.Fatal(err.Error())
    }
    defer testerator.SpinDown()

    os.Exit(m.Run())
}

私が遭遇した問題

SpinDown() してもリセットされない

次の3つのパッケージをインポートしていない場合それぞれ該当するクリーナーが動作しません。

import (
    _ "github.com/favclip/testerator/datastore"
    _ "github.com/favclip/testerator/memcache"
    _ "github.com/favclip/testerator/search"
)

並列試験したい

GAEに限らずGolangでは各テストの頭で t.Parallel() を呼ぶことで並列試験を行うことができます。

が、何も考えず動かすとDatastoreが競合するのでNamespaceを使いテスト毎に環境を分離する必要があります。

func TestSomething(t *testing.T) {
    t.Parallel()
    _, ctx, err := testerator.SpinUp()
    if err != nil {
        t.Fatal(err.Error())
    }
    defer testerator.SpinDown()
    ctx, err = appengine.Namespace(ctx, "something-namespace")

    // do something test

実アプリ内でNamespaceを使っているとこの手段が使えないので、その場合は大人しくCIの独立環境で並列試験するしかないでしょう。

変更した部分だけテストしたい

下記のようなGitコマンドで変更があったファイルを抽出できますので、あとは grep, sed, sort, uniq 等を駆使することで変更があったパッケージのみのテストが実行できます。

$ git diff master..HEAD --diff-filter ACMR --name-only