由于aetest非常慢,因此想办法解决的问题
aetest是什么
以下是GAE/Go用于本地测试的包。
虽然GAE已经转变为第二代,并且将来不太可能被新用户使用,但我希望将过去的努力记录下来作为纪念。
以下是GAE的测试样例代码。aetest会在测试执行时在本地启动一个具有GAE特定功能的服务器,通过使用包含该服务器信息的上下文,可以进行测试。
在示例代码中,使用了GAE的Memcache功能。
package main
import (
"google.golang.org/appengine"
"google.golang.org/appengine/aetest"
"google.golang.org/appengine/memcache"
"testing"
)
func TestAeTest(t *testing.T) {
inst, err := aetest.NewInstance(nil)
if err != nil {
t.Fatal(err)
}
defer inst.Close()
req, err := inst.NewRequest("GET", "/", nil)
ctx := appengine.NewContext(req)
it := &memcache.Item{
Key: "some-key",
Value: []byte("some-value"),
}
err = memcache.Set(ctx, it)
if err != nil {
t.Fatalf("Set err: %v", err)
}
it, err = memcache.Get(ctx, "some-key")
if err != nil {
t.Fatalf("Get err: %v; want no error", err)
}
if g, w := string(it.Value), "some-value" ; g != w {
t.Errorf("retrieved Item.Value = %q, want %q", g, w)
}
}
在aetest.NewInstance()中,所做的是将简单的GAE应用程序设置创建在临时区域中。
设置在内部定义,并以最少的设置作为GAE/Go应用程序启动。
const appYAMLTemplate = `
application: %s
version: 1
runtime: go111
handlers:
- url: /.*
script: _go_app
`
const appSource = `
package main
import "google.golang.org/appengine"
func main() { appengine.Main() }
`
在这里,GAE的开发服务器命令已经设置好,并且通过子进程启动了类似于`dev_appserver.py /var/folders/1w/9n3yj1g95g12zgjhm9xxcypc0000gp/T/tmp4C_Mm8appengine-go-bin/app.yml`的命令。
开发服务器启动后,会在标准输出中显示API等的URL。我们采用正则表达式将其提取出来,并向其中发送datastore等命令。这是一个相当狂野的实现方式。
INFO 2019-10-10 06:39:59,912 devappserver2.py:278] Skipping SDK update check.
WARNING 2019-10-10 06:39:59,912 devappserver2.py:294] DEFAULT_VERSION_HOSTNAME will not be set correctly with --port=0
INFO 2019-10-10 06:40:00,098 datastore_emulator.py:155] Starting Cloud Datastore emulator at: http://localhost:21206
WARNING 2019-10-10 06:40:00,100 simple_search_stub.py:1196] Could not read search indexes from /var/folders/1w/9n3yj1g95g12zgjhm9xxcypc0000gp/T/appengine.testapp.hoshina/search_indexes
INFO 2019-10-10 06:40:01,231 datastore_emulator.py:161] Cloud Datastore emulator responded after 1.132465 seconds
INFO 2019-10-10 06:40:01,232 api_server.py:275] Starting API server at: http://localhost:64020
INFO 2019-10-10 06:40:01,237 api_server.py:265] Starting gRPC API server at: http://localhost:64022
INFO 2019-10-10 06:40:01,312 dispatcher.py:256] Starting module "default" running at: http://localhost:64023
INFO 2019-10-10 06:40:01,316 admin_server.py:150] Starting admin server at: http://localhost:64025
INFO 2019-10-10 06:40:03,328 stub_util.py:357] Applying all pending transactions and saving the datastore
INFO 2019-10-10 06:40:03,328 stub_util.py:360] Saving search indexes
INFO 2019-10-10 06:40:04,252 instance.py:294] Instance PID: 57474
var apiServerAddrRE = regexp.MustCompile(`Starting API server at: (\S+)`)
var adminServerAddrRE = regexp.MustCompile(`Starting admin server at: (\S+)`)
最初我误解了,aetest并不会为我们启动目标应用程序,它只是提供了使用appengineAPI所需的最基本资源。
所以它并不能模拟应用程序的路由,因此如果我们创建了一个模拟了实际应用程序的请求并通过http.Client的Do(req)方法进行测试,是无法正常工作的,需要注意。
aetest.NewInstance()这个函数表现出来的行为是每次都会启动一个子进程,所以速度相当慢。
在下面的测试中,每个测试都要花费大约5秒钟的时间,其中大部分时间都用于启动dev_appserver.py。
package main
import (
"google.golang.org/appengine"
"google.golang.org/appengine/aetest"
"testing"
)
func TestAppID(t *testing.T) {
ctx, done, err := aetest.NewContext()
if err != nil {
t.Fatal(err)
}
defer done()
val := appengine.AppID(ctx)
if val != "testapp" {
t.Fatalf("got: %v\nwant: %v", val, "testapp")
}
}
func TestDefaultVersionHostname(t *testing.T) {
ctx, done, err := aetest.NewContext()
if err != nil {
t.Fatal(err)
}
defer done()
val := appengine.DefaultVersionHostname(ctx)
if val != "" {
t.Fatalf("got: %v\nwant: %v", val, "")
}
}
基本上,实例应该是可重复使用的,因此可以将实例创建为包变量并保持,以便在需要时返回先前创建的实例(如果设置相同),从而避免每次启动。
package main
import (
"context"
"fmt"
"google.golang.org/appengine"
"google.golang.org/appengine/aetest"
"os"
"testing"
)
func TestAppID(t *testing.T) {
ctx, done := NewContext(nil)
defer done()
val := appengine.AppID(ctx)
if val != "testapp" {
t.Fatalf("got: %v\nwant: %v", val, "testapp")
}
}
func TestDefaultVersionHostname(t *testing.T) {
ctx, done := NewContext(nil)
defer done()
val := appengine.DefaultVersionHostname(ctx)
if val != "" {
t.Fatalf("got: %v\nwant: %v", val, "")
}
}
var instances map[string]aetest.Instance
func NewContext(opt *aetest.Options) (context.Context, func()) {
var err error
key := fmt.Sprintf("%#v", opt)
//設定が異なる場合違うインスタンスを作成する
if _, ok := instances[key]; !ok {
instances[key], err = aetest.NewInstance(opt)
if err != nil {
panic(err)
}
}
req, err := instances[key].NewRequest("GET", "/", nil)
if err != nil {
panic(err)
}
ctx := appengine.NewContext(req)
return ctx, func() {
//なにもしない
}
}
func TestMain(m *testing.M) {
instances = map[string]aetest.Instance{}
//パッケージ内のテストを終了
status := m.Run()
//goroutineがインスタンスの終了を待っているので最後に終了する
for _, i := range instances {
i.Close()
}
os.Exit(status)
}
需要注意的是,由于aetest.Instance在等待子进程继续运行的goroutine中,所以只要有一个实例启动,就会继续等待goroutine结束,导致无法结束测试进程。
為了結束測試,必須確保在最後終止實例。
然而,这里有一个问题。如果在测试中出现 panic,那么由于实例没有在 m.Run() 之后退出,测试将无法结束。
func TestPanic(t *testing.T) {
panic("from TestPanic")
}
所以,如果发生恐慌(panic),可以通过调用recover()来捕获它并终止实例,所以我认为在TestMain中使用defer就可以了,于是我对TestMain进行了如下修改。
func TestMain(m *testing.M) {
status := 0
defer func() {
for _, i := range instances {
i.Close()
}
os.Exit(status)
}()
instances = map[string]aetest.Instance{}
status = m.Run()
//goroutineがインスタンスの終了を待っているので最後に終了する
for _, i := range instances {
i.Close()
}
}
然而,这个变更毫无意义。
通常情况下,当测试发生panic时,尝试输出测试结果并立即终止。但是,如果在testing以外的goroutine中,通道处于等待输入的状态等停止状态时,没有发送完整的测试完成信号,测试进程就无法终止。
即使结束信号返回了,因为每个测试本身就在 goroutine 中执行,并且无法跨越 goroutine 进行 recover,所以这段代码是多余的且没有意义的。
最后该怎么办呢?需要使goroutine不必等待服务器的结束,或者需要使panic时也能恢复控制。
如果选择前者的话,可以在TestMain中创建所有服务器的各种情况,就可以解决问题。但是,如果设置有很多种情况,可能会很困难。
另外,即使发生panic,也可以通过明确地返回testing的控制来向父goroutine传达结束的消息。
func TearDown(t *testing.T) {
if err := recover(); err != nil {
debug.PrintStack() //スタックトレースが出ないので自分で吐いている
t.Fatal(err)
}
}
func TestPanic(t *testing.T) {
defer TearDown(t)
panic("from TestPanic")
}
虽然是一个矮小的话题,但如果测试依赖于外部进程并试图在测试执行时管理其状态,可能会陷入类似的情况。(例如,在测试执行期间想以不同的模式启动数据库)
在那种情况下,如果试图通过TestMain或类似的方式进行通用的前处理和后处理,可能会陷入异常的困境。