以前沒有針對 Spanner 的數據層單元測試框架,後端服務在完成編碼後 DAO 層單測的數據,基本經過 Mock 的方式,或對測試數據庫的讀寫來實現。這種單測手段存在一些問題:node
目前公司部分服務的單元測試依賴以上兩種方式, 本文介紹一種經過模擬 Spanner 的方式, 來解決以上問題。linux
目前有兩個業界用得比較多的 Spanner 的模擬器:git
本文僅介紹 cloud-spanner-emulator。github
注:使用 handy-spanner 做爲單測和 BDD 測試 的數據存儲也是一個不錯的選擇, 關於 BDD 測試 , 可參考 Go 項目的 BDD 實踐
cloud-spanner-emulator 支持如下幾種方式完成初始化:sql
本地能夠經過 gcloud 的方式調用 spanner emulator:docker
gcloud config configurations create emulator gcloud config set auth/disable_credentials true gcloud config set project your-project-id gcloud config set api_endpoint_overrides/spanner http://localhost:9010/ gcloud spanner instances create test-instance \ --config=emulator-config --description="Test Instance" --nodes=1
線上經過預編譯 linux 二進制程序 + docker 鏡像提供 spanner emulator 環境。shell
對於線上和線下環境 spanner 模擬器初始化步驟的差別,可在項目中的 unit_test.sh 腳本作處理:數據庫
# export spanner emulator env if [ "x_$NODE_ENV" != "x_local" ]; then /spanner/emulator/init.sh /spanner/emulator/start.sh fi export SPANNER_EMULATOR_HOST=localhost:9010
以 Go 項目爲例, 咱們的開發腳手架封裝有一個 spannerClient,對 DB 的 CURD 操做經過同一個全局的 spannerClient 完成。因此要接入 Cloud Spanner Emulator 僅僅須要在初始化 spannerClient 連上測試的 DB實例:segmentfault
var db = "projects/emulator-project/instances/emulator-instance/databases/example-db" func InitTestingSpannerClient() { cli, err := spanner.NewClient(context.TODO(), db, spanner2.ClientConfig{SessionPoolConfig: spanner2.SessionPoolConfig{ MaxOpened: 200, MinOpened: 5, MaxIdle: 10, }}) if err != nil { logger.Critical(context.TODO(), "Connecting to spanner emulator failed: %v!", zap.Error(err)) } spannerClient = cli }
同時咱們須要初始化一個 AdminClient, 用於調用Cloud Spanner數據庫管理API, 進行 DDL 操做:後端
func InitTestingSpannerAdminClient() { emulatorAddr := os.Getenv("SPANNER_EMULATOR_HOST") var opts []option.ClientOption opts = append( opts, option.WithEndpoint(emulatorAddr), option.WithGRPCDialOption(grpc.WithInsecure()), option.WithoutAuthentication(), ) var err error TestingSpannerAdminClient, err = dbadmin.NewDatabaseAdminClient(context.Background(), opts...) if err != nil { panic(fmt.Sprintf("Setting up testing's Spanner admin client failed: %v", err)) } }
初始化 spannerClient 和 spannerAdminClient 以後, 在作單元測試以前建立數據表,並灌數據到 Emulator。 封裝瞭如下函數,經過原生 Spanner SDK 直接操做數據:
建立 Tables:
func CreateTables(ctx context.Context, statements []string) error { matches := regexp.MustCompile("^(.*)/databases/(.*)$").FindStringSubmatch(db) if matches == nil || len(matches) != 3 { return fmt.Errorf("Invalid database id %s", db) } // if db.State == "READY" _, err := TestingSpannerAdminClient.GetDatabase(ctx, &dbadminpb.GetDatabaseRequest{ Name: db, }) if err == nil { return nil } op, err := TestingSpannerAdminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{ Parent: matches[1], CreateStatement: "CREATE DATABASE `" + matches[2] + "`", ExtraStatements: statements, }) if err != nil { return err } if _, err := op.Wait(ctx); err != nil { return err } return nil }
更新 Schema:
func UpdateMockData(ctx context.Context, statements ...string) error { op, err := TestingSpannerAdminClient.UpdateDatabaseDdl(ctx, &dbadminpb.UpdateDatabaseDdlRequest{ Database: dbName, Statements: statements, }) if err != nil { return err } return op.Wait(mockCtx) }
刪除 Mock 數據:
func DeleteMockData(ctx context.Context, table string, key interface{}) (err error) { _, err = spannerClient.NativeClient().Apply(ctx, []*spanner2.Mutation{ spanner2.Delete(table, spanner2.Key{key}), }) return }
新增 Mock 數據:
func InsertMockData(table string, keys []string, vals []interface{}) (err error) { _, err = spannerClient.NativeClient().Apply(context.TODO(), []*spanner2.Mutation{spanner2.Insert(table, keys, vals)}) return }
查詢 Mock 數據:
func GetMockData(query string, params map[string]interface{}) ([]interface, error) { stmt := spanner2.NewStatement(query) stmt.Params = params rows := spannerClient.NativeClient().Single().Query(context.TODO(), stmt) var ret []interface{} err := rows.Do(func(row *spanner2.Row) error { var meta string if err := row.Columns(&meta); err != nil { return fmt.Errorf("decode Colunms error: %v", err) } ret = append(ret, meta) return nil }) if err != nil { return ret, err } return ret, nil
本文主要介紹了官方的 Cloud Spanner Emulator: 如何初始化環境、進行單元測試以及對 Mock 數據的處理 。經過 Spanner Emulator, 來彌補 GCP Spanenr 在 DAO 層單元測試上的缺失。