Go 本地单元测试

本地单元测试在您的环境内部运行,而无需访问远程组件。 App Engine 提供的测试实用程序使用在本地实现的 Cloud Datastore 及其他 App Engine 服务

开发服务在本地模拟实际服务的行为,以进行测试。例如,您可以按照编写 Cloud Datastore 和 Memcache 测试中显示的数据存储区用法,对数据存储区代码进行测试,而无需向实际数据存储区发送任何请求。在数据存储区单元测试过程中存储的任何实体均会在本地存储,并会在测试运行之后删除。您无需依赖数据存储区本身便可快速运行小型测试。

本文档介绍了如何使用 Go 测试软件包针对本地 App Engine 服务编写单元测试。

Go 测试软件包

您可以使用 goapp 工具自动下载、构建、测试 Go 软件包。goappGo 版 App Engine SDK 的一部分。

您可以结合使用 goapp test 命令与标准 Go testing 软件包,针对您的应用代码运行单元测试。如需了解对 Go 进行测试的背景信息,请参阅如何编写 Go 代码的“测试”部分和测试软件包参考文档

单元测试包含在以后缀 _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 时,系统会在子进程中启动 dev_appserver.py,此操作将用于在测试期间处理 API 调用。您可以调用 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 测试

使用 aetest 软件包创建 context.Context 后,您可以轻松测试使用数据存储区或 Memcache 的代码:在您的测试中,调用 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。