はじめに
最近Spannerを使用しているプロジェクトで開発していることが多いのですが、Spanner Emulatorのレコード検証を行うのが面倒だなと思うことがよくありました。 特に E2E テスト・統合テストで UI 操作 → DB 側の検証という流れを自動化したいとき、SQL を都度書くのではなく、もっと直観的に「このテーブルはこのレコードが入ってて、このテーブルの件数はこう」という期待値を管理したいと思っていました。 そこで、Playwrightの1ステップのようにSpanner Emulatorのレコード検証を行うことができるライブラリを作りました。
ローカルの Spanner エミュレータ環境で動かすことを前提に、「期待値を JSON(もしくは定義ファイル)に書き、それを実データと比較する」と言う仕組みです。
概要
Playwrightのテストケース内で気軽に呼びたかったので、npmライブラリとして作成しました。 使い方はテーブルレコードの期待値をJSONで定義し、それを実データと比較します。 例えば、以下のようなJSONを定義します。
{
"tables": {
"Users": {
"rows": [
{
"UserID": "user-001",
"Name": "Alice Example",
"Email": "alice@example.com",
"Status": 1,
"CreatedAt": "2024-01-01T00:00:00Z"
}
],
"count": 1
},
"Productypescript": {
"rows": [
{
"ProductID": "product-001",
"Name": "Example Product",
"Price": 1999,
"IsActive": true,
"CategoryID": null,
"CreatedAt": "2024-01-01T00:00:00Z"
}
]
}
}
}
その後、Playwrightのテスト内でspannerAssertのクライアントを作成し、assertメソッドを呼び出します。 そうすると、引数に与えたJSONの構造を解析して、期待値と実データを比較します。
import { createSpannerAssert } from 'spanner-assert'
import expectations from './expectations.json' with { type: 'json' }
const spannerAssert = createSpannerAssert({
connection: {
projectId: 'your-project-id',
instanceId: 'your-instance-id',
databaseId: 'your-database',
emulatorHost: '127.0.0.1:9010',
},
})
await spannerAssert.assert(expectations)
実際のレコードが期待値と異なる場合はjest-diffを用いた期待値と実値の比較が下記のように表示されます。
SpannerAssertionError: 1 expected row(s) not found in table "Users".
- Expected
+ Actual
Array [
Object {
- "Name": "Alice",
+ "Name": "Invalid Name",
},
]
SSOTで期待値を管理することも可能
JSONを期待値として管理するので、以下のように、Playwrightのテストのステップとして期待値のJSONの値を使用することもできます。
await step('ユーザーを作成', async () => {
await page.goto('/users/new')
await page.fill("input[name='name']", expectations.tables.Users.rows[0].Name)
await page.fill(
"input[name='email']",
expectations.tables.Users.rows[0].Email,
)
await page.click("button[type='submit']")
await spannerAssert.assert(expectations)
})
こうすることで、期待値をSSOTのように使用することもでき、UIに入力する値と期待値を同じJSONで管理することができます。
複数DBにも対応
spannerAssertのインスタンスは複数作成できるので、複数のSpanner Emulatorのデータベースを比較することもできます。
// DB1
const spannerAssert = createSpannerAssert({
connection: {
projectId: 'your-project-id',
instanceId: 'your-instance-id',
databaseId: 'your-database',
emulatorHost: '127.0.0.1:9010',
},
})
// DB2
const spannerAssert2 = createSpannerAssert({
connection: {
projectId: 'your-project-id',
instanceId: 'your-instance-id',
databaseId: 'your-database',
emulatorHost: '127.0.0.1:9010',
},
})
await spannerAssert.assert(expectations)
await spannerAssert2.assert(expectations2)
そのため、管理画面などの複数のDBを操作するプロダクトにおいても、インスタンス作成時の接続情報に接続したいDBの情報を指定することで、簡単に期待値を管理することができます。 現在は文字列、数値、真偽値、null、配列にのみ対応しています。Spannerの型は色々あるので、必要に応じて追加していきたいと思います。
npmライブラリのPublishについて
npmライブラリを公開する場合、サプライチェーン攻撃に対しての対応が必須です。 そのため、今回作成するライブラリは以下のような対策を行いました。
- npmのOIDCを使用したリリース
- renovateによる依存パッケージの更新
- ghalintによるセキュリティリスクの検知
- setup-npm-trusted-publishによるnpmのOIDCの設定の簡略化
- huskyによるsecretlintの設定
- ghaのconcurrency + timeoutの設定
- zizmorによるセキュリティリスクの検知
- publintによるパッケージのリリース
- Require two-factor authentication and disallow tokensの設定
ライブラリ開発時に便利だったこと
E2Eは早めにCIに組み込んでおくべきです。今回はDockerでSpannerのEmulatorを起動してseed投入などをする必要があったため、Taskfileを使用したE2Eテストを早めに組み込んでおくことで、ローカルでの開発時にもワンコマンドでE2Eテストを実行でき非常に便利でした。
リリース方法などは個人的 npm ライブラリ開発Tips 2025 に記載しました。
まとめ
Playwrightの1ステップでSpannerのレコード検証を行うライブラリを作りました。 フォームへの入力値とDBの期待値をSSOTで書けるのは開発者体験の向上につながるかなと思います。