ローカルでの Go の単体テスト

ローカル単体テストは、リモート コンポーネントにアクセスしない環境内で実行します。App Engine には、Cloud Datastore や他の App Engine サービスのローカル実装を使用するテスト ユーティリティが用意されています。

開発サービスは、実際のサービスの動作をテスト目的でローカルにシミュレートします。たとえば、Cloud Datastore と Memcache のテストを作成するで示されているデータストアの使用方法では、実際のデータベースに一切リクエストを行うことなく、データストア コードをテストできます。データストアの単体テストで生成されるエンティティはローカルに格納され、テストの実行後に削除されます。データストア自体に依存することはないため、簡単なテストをすばやく実行できます。

このドキュメントでは、Go の testing パッケージを使用して、ローカルの App Engine サービスに対する単体テストを作成する方法を説明します。

Go の testing パッケージ

Go パッケージのダウンロード、ビルド、テストは、goapp ツールを使用して自動化できます。goapp は、App Engine SDK for Go の一部です。

goapp test コマンドと標準の Go testing パッケージの組み合わせを使用して、アプリケーション コードに対して単体テストを実行できます。Go でのテストの背景情報については、How to Write Go Code の「Testing」セクションと Package testing リファレンスをご覧ください。

単体テストは、_test.go という接尾辞で終わるファイルに保存されています。たとえば、composeNewsletter という名前の関数のテストを行うとします。この関数は、*mail.Message を返します。次の newsletter_test.go ファイルは、この関数の簡単なテストを示しています。

package newsletter

import (
	"reflect"
	"testing"

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

func TestComposeNewsletter(t *testing.T) {
	want := &mail.Message{
		Sender:  "newsletter@appspot.com",
		To:      []string{"User <user@example.com>"},
		Subject: "Weekly App Engine Update",
		Body:    "Don't forget to test your code!",
	}
	if msg := composeNewsletter(); !reflect.DeepEqual(msg, want) {
		t.Errorf("composeMessage() = %+v, want %+v", msg, want)
	}
}

このテストは、パッケージのディレクトリ内から goapp test コマンドを使用して起動します。

goapp test

goapp ツールは App Engine SDK のルート ディレクトリにあります。テストを簡単に実行できるように、このディレクトリをシステムの PATH 変数に配置することをおすすめします。

aetest パッケージ

App Engine サービスに対する多くの関数呼び出しでは、引数として context.Context が必要です。SDK で提供される appengine/aetest パッケージを使用すると、開発環境で提供されるサービスを使用してテストを実行するための偽の context.Context を作成できます。

import (
	"testing"

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

func TestWithContext(t *testing.T) {
	ctx, done, err := aetest.NewContext()
	if err != nil {
		t.Fatal(err)
	}
	defer done()

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

aetest.NewContext の呼び出しにより、テスト中に API 呼び出しを処理するために使用されるサブプロセスで、dev_appserver.py が開始されます。このサブプロセスは、done への呼び出しでシャットダウンします。

基になるインスタンスをより細かく制御するには、代わりに aetest.NewInstance を使用します。これにより、複数のコンテキストを作成し、それらを http.Request オブジェクトに関連付けることができます。

import (
	"errors"
	"testing"

	"golang.org/x/net/context"

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

func TestMyFunction(t *testing.T) {
	inst, err := aetest.NewInstance(nil)
	if err != nil {
		t.Fatalf("Failed to create instance: %v", err)
	}
	defer inst.Close()

	req1, err := inst.NewRequest("GET", "/gophers", nil)
	if err != nil {
		t.Fatalf("Failed to create req1: %v", err)
	}
	c1 := appengine.NewContext(req1)

	req2, err := inst.NewRequest("GET", "/herons", nil)
	if err != nil {
		t.Fatalf("Failed to create req2: %v", err)
	}
	c2 := appengine.NewContext(req2)

	// Run code and tests with *http.Request req1 and req2,
	// and context.Context c1 and c2.
	// ...
}

詳細については、aetest パッケージ リファレンスをご覧ください。

Cloud Datastore と Memcache のテストを作成する

データストアまたは memcache を使用するコードのテストは、aetest パッケージで context.Context を作成したら簡単です。テストで aetest.NewContext を呼び出し、テスト対象の関数に渡すコンテキストを作成します。

SDK の transaction デモ アプリケーションには、テストを可能にするコードの構成例と、データストアを使用するコードのテスト方法が含まれています。

func TestWithdrawLowBal(t *testing.T) {
	ctx, done, err := aetest.NewContext()
	if err != nil {
		t.Fatal(err)
	}
	defer done()
	key := datastore.NewKey(ctx, "BankAccount", "", 1, nil)
	if _, err := datastore.Put(ctx, key, &BankAccount{100}); err != nil {
		t.Fatal(err)
	}

	err = withdraw(ctx, "myid", 128, 0)
	if err == nil || err.Error() != "insufficient funds" {
		t.Errorf("Error: %v; want insufficient funds error", err)
	}

	b := BankAccount{}
	if err := datastore.Get(ctx, key, &b); err != nil {
		t.Fatal(err)
	}
	if bal, want := b.Balance, 100; bal != want {
		t.Errorf("Balance %d, want %d", bal, want)
	}
}

このテストは、goapp test コマンドを使用して実施できます。

goapp test ./demos/transaction

Memcache のテストも同様のパターンに従って実行します。テストで Memcache の初期状態を設定し、テスト対象の関数を実行して、関数によって予期したとおりに Memcache がクエリまたは変更されていることを確認します。