Spanner のレコードをYAML で検証できるGo製 CLIツールを作った際の学び
背景
Spanner のデータ検証を楽にしたいなと思い、YAML で期待値を記述してレコードが期待値を満たしているか確認できる CLI ツールを作ってみました。
動作例
まず、テーブルごとに、複数行・複数カラムの期待値を YAML で記述します。
tables:
Users:
columns:
- UserID: "user-001"
Name: "Alice Johnson"
Email: "alice@example.com"
Status: 1
CreatedAt: "2024-01-01T00:00:00Z"
Products:
columns:
- ProductID: "prod-001"
Name: "Laptop Computer"
Price: 150000
IsActive: true
CategoryID: "cat-electronics"
CreatedAt: "2024-01-01T00:00:00Z"
Books:
columns:
- BookID: "book-001"
Title: "The Great Gatsby"
Author: "F. Scott Fitzgerald"
PublishedYear: 1925
JSONData: '{"genre": "Fiction", "rating": 4.5}'
その後、以下のコマンドを実行します。
spalidate --project <your-project> --instance <your-instance> --database <your-database> ./validation.yaml
もし期待値と一致しない場合は、以下のようなログが出力されます。
2025/09/13 19:09:25 ERRO ✖️ table Books: expected row does not match
column mismatch: 1
1) column: JSONData
▸ expected: {"genre":"Fiction","rating":4.5}
▸ actual: {"genre": "invalid", "rating": 4.5}
2025/09/13 19:09:25 ERRO ✖️ table Products: expected row does not match
column mismatch: 1
1) column: CategoryID
▸ expected: cat-electronics
▸ actual: cat-invalid
2025/09/13 19:09:25 ERRO ✖️ table Users: expected row does not match
column mismatch: 2
1) column: Email
▸ expected: alice@example.com
▸ actual: alice@invalid.com
2) column: Status
▸ expected: 1
▸ actual: 9999
CLIツールについて
処理自体は簡単です。コマンドライン引数で与えられる Project/Instance/Database を使って Spanner に接続します。その後、Spanner Client を使ってデータを取得し、go の reflect を使って期待値と比較しています。
Spanner Emulator+統合テスト
このツールをテストする場合、並列にテストを行いたければ、テストケースごとにデータベースをそれぞれ用意する必要があります。そうしないと、テストケースでデータを入力した場合に他のテストが落ちてしまうのと、テスト実行時間がすぐ長くなってしまいます。
Spanner Emulator を使用したテストはどうするのがよいかなと色々眺めていると、公式レポジトリに以下の内容が記述されていました。
What is the recommended test setup?
Use a single emulator process and create a Cloud Spanner instance within it. Since creating databases is cheap in the emulator, we recommend that each test bring up and tear down its own database. This ensures hermetic testing and allows the test suite to run tests in parallel if needed.
そんなこと言っても、どうやってテストケースごとにセットアップ + tear down するのがよいのか...?、データベース名をどうにかテストケース毎に切り替えれるようにしないといけないしな...と思っていると、以下の素晴らしい情報に出会いました。
spanemuboost を使うと、インスタンスとデータベースは自動的に作成され、設定済みの Spanner クライアントをすぐに使うことができます。
また、テストケースごとに異なるデータベースを使う推奨構成にも対応しています。
コンテナの起動時にはインスタンスのみをセットアップし、ランダムな名前のデータベースとそれに対するクライアントをテストケースごとに生成することが下記のように手軽に行えます。
spanemuboost というライブラリを使用すれば、t.Parallel()
を付与しつつテストが書けるということがわかりました。実際に試してみると、database名はWithRandomDatabaseID
でcm903okki1pr2k18ssqzvslltqq9kc
のような文字列を生成してくれるので、独立したデータベースが作成できることが確認できました。
今回はデータベース名を指定するライブラリのため、spanemuboost.NewClients
が返すDatabaseID
を引数に渡せば、実現したい独立したemulatorを使用したテストが可能になります。
// integration_test.go(抜粋)
t.Run("TestWithExistingValidationFile", func(t *testing.T) {
t.Parallel()
clients, clientsTeardown, err := spanemuboost.NewClients(ctx, emulator,
spanemuboost.EnableDatabaseAutoConfigOnly(),
spanemuboost.WithRandomDatabaseID(),
spanemuboost.WithSetupDDLs(ddls),
)
if err != nil {
t.Fatal(err)
}
defer clientsTeardown()
if err := initializeTestData(ctx, clients.Client); err != nil {
t.Fatal(err)
}
output, err := runSpalidateWithFile("test_validation.yaml", true, clients.ProjectID, clients.InstanceID, clients.DatabaseID) // DatabaseIDにランダムな値が入るので独立してテストができる
if err != nil {
t.Fatalf("Validation failed: %v\nOutput: %s", err, output)
}
if !strings.Contains(output, "Validation passed for all tables") {
t.Errorf("Expected success message, got: %s", output)
}
})
まとめ
Spanner のレコードを自動で検証できるCLIツールを作ってみました。正直 Select 文で検証してもよいのですが、宣言的に期待値を保存しておきたいのもあり、今回は yaml を使用するようにしました。spanemuboostの恩恵を受け、データベースを使用しながらも並列なテストを実行できています。
まだ Spanner のいくつかの型には対応していないので、実際のユースケースと相談しながら対応していきたいと思います。
参考
Discussion