{"id":32787400,"url":"https://github.com/nu0ma/spanner-assert","last_synced_at":"2025-11-05T06:03:21.968Z","repository":{"id":319205898,"uuid":"1075291052","full_name":"nu0ma/spanner-assert","owner":"nu0ma","description":"Validate Google Cloud Spanner Emulator data against expectations written in JSON","archived":false,"fork":false,"pushed_at":"2025-11-02T06:49:58.000Z","size":482,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-02T07:17:59.906Z","etag":null,"topics":["e2e","playwright","spanner","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/spanner-assert","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nu0ma.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-10-13T09:50:40.000Z","updated_at":"2025-11-02T06:49:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"35a69be6-68f7-47f4-8c9a-9942d8e15b96","html_url":"https://github.com/nu0ma/spanner-assert","commit_stats":null,"previous_names":["nu0ma/spanner-assert"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/nu0ma/spanner-assert","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nu0ma%2Fspanner-assert","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nu0ma%2Fspanner-assert/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nu0ma%2Fspanner-assert/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nu0ma%2Fspanner-assert/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nu0ma","download_url":"https://codeload.github.com/nu0ma/spanner-assert/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nu0ma%2Fspanner-assert/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":282769253,"owners_count":26724204,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-05T02:00:05.946Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["e2e","playwright","spanner","typescript"],"created_at":"2025-11-05T06:01:12.615Z","updated_at":"2025-11-05T06:03:21.961Z","avatar_url":"https://github.com/nu0ma.png","language":"TypeScript","readme":"# spanner-assert\n\n[![npm version](https://img.shields.io/npm/v/spanner-assert)](https://www.npmjs.com/package/spanner-assert)\n[![CI](https://github.com/nu0ma/spanner-assert/actions/workflows/ci.yaml/badge.svg)](https://github.com/nu0ma/spanner-assert/actions/workflows/ci.yaml)\n[![License](https://img.shields.io/npm/l/spanner-assert)](https://github.com/nu0ma/spanner-assert/blob/main/LICENSE)\n\nValidate Google Cloud Spanner **emulator** data against expectations written in JSON. Lightweight Node.js testing library for E2E workflows, fast feedback loops.\n\n\u003e ⚠️ **This library only supports Cloud Spanner emulator** - designed for testing environments, not production databases.\n\n## Install\n\n```bash\nnpm install -D spanner-assert\n```\n\n## Quick Start\n\n1. Start the Spanner emulator and note the connection settings.\n\n2. Create an expectations JSON file:\n\n```json\n{\n  \"tables\": {\n    \"Users\": {\n      \"rows\": [\n        {\n          \"UserID\": \"user-001\",\n          \"Name\": \"Alice Example\",\n          \"Email\": \"alice@example.com\",\n          \"Status\": 1,\n          \"CreatedAt\": \"2024-01-01T00:00:00Z\"\n        }\n      ]\n    },\n    \"Products\": {\n      \"rows\": [\n        {\n          \"ProductID\": \"product-001\",\n          \"Name\": \"Example Product\",\n          \"Price\": 1999,\n          \"IsActive\": true,\n          \"CategoryID\": null,\n          \"CreatedAt\": \"2024-01-01T00:00:00Z\"\n        }\n      ]\n    },\n    \"Books\": {\n      \"rows\": [\n        {\n          \"BookID\": \"book-001\",\n          \"Title\": \"Example Book\",\n          \"Author\": \"Jane Doe\",\n          \"PublishedYear\": 2024,\n          \"JSONData\": \"{\\\"genre\\\":\\\"Fiction\\\",\\\"rating\\\":4.5}\"\n        }\n      ]\n    }\n  }\n}\n```\n\nEach table lists expected rows as an array.\nAdd an optional `count` field when you also want to assert the total number of rows returned.\n\n3. Run the assertion from a script:\n\n```ts\nimport { createSpannerAssert } from \"spanner-assert\";\nimport expectations from \"./expectations.json\" with { type: \"json\" };\n\nconst spannerAssert = createSpannerAssert({\n  connection: {\n    projectId: \"your-project-id\",\n    instanceId: \"your-instance-id\",\n    databaseId: \"your-database\",\n    emulatorHost: \"127.0.0.1:9010\",\n  },\n});\n\nawait spannerAssert.assert(expectations);\n```\n\nOn success you get no output (or your own logging) because all tables matched.\n\n### If not successful\n\n```text\nSpannerAssertionError: 1 expected row(s) not found in table \"Users\".\n  - Expected\n  + Actual\n\n  Array [\n    Object {\n-     \"Name\": \"Alice\",\n+     \"Name\": \"Invalid Name\",\n    },\n  ]\n```\n\nAn error is thrown with a color-coded diff showing expected vs actual values (using jest-diff).\n\n## Example using Playwright\n\nHere's a practical example of using `spanner-assert` in Playwright E2E tests to validate database state after user interactions:\n\n```ts\nimport { test, expect } from \"@playwright/test\";\nimport { createSpannerAssert } from \"spanner-assert\";\nimport userCreatedExpectations from \"./test/expectations/user-created.json\" with { type: \"json\" };\nimport profileUpdatedExpectations from \"./test/expectations/profile-updated.json\" with { type: \"json\" };\nimport productInventoryExpectations from \"./test/expectations/product-inventory.json\" with { type: \"json\" };\n\ntest.describe(\"User Registration Flow\", () =\u003e {\n  let spannerAssert;\n\n  test.beforeAll(async () =\u003e {\n    spannerAssert = createSpannerAssert({\n      connection: {\n        projectId: \"your-project-id\",\n        instanceId: \"your-instance-id\",\n        databaseId: \"your-database\",\n        emulatorHost: \"127.0.0.1:9010\",\n      },\n    });\n  });\n\n  test(\"should create user record after registration\", async ({ page }) =\u003e {\n    // 1. Perform UI actions\n    await page.goto(\"https://your-app.com/register\");\n    await page.fill('[name=\"email\"]', \"alice@example.com\");\n    await page.fill('[name=\"name\"]', \"Alice Example\");\n    await page.click('button[type=\"submit\"]');\n\n    await expect(page.locator(\".success-message\")).toBeVisible();\n\n    // 2. Validate database state with spanner-assert\n    await spannerAssert.assert(userCreatedExpectations);\n  });\n\n  test(\"should update user profile\", async ({ page }) =\u003e {\n    // Navigate and update profile\n    await page.goto(\"https://your-app.com/profile\");\n    await page.fill('[name=\"bio\"]', \"Software engineer\");\n    await page.click('button:has-text(\"Save\")');\n\n    await expect(page.locator(\".success-notification\")).toBeVisible();\n\n    // Verify database was updated correctly\n    await spannerAssert.assert(profileUpdatedExpectations);\n  });\n\n  test(\"should create product and verify inventory\", async ({ page }) =\u003e {\n    // Admin creates a new product\n    await page.goto(\"https://your-app.com/admin/products\");\n    await page.fill('[name=\"productName\"]', \"Example Product\");\n    await page.fill('[name=\"price\"]', \"1999\");\n    await page.check('[name=\"isActive\"]');\n    await page.click('button:has-text(\"Create Product\")');\n\n    await expect(page.locator(\".product-created\")).toBeVisible();\n\n    // Validate both Products and Inventory tables\n    await spannerAssert.assert(productInventoryExpectations);\n  });\n});\n```\n\n**Example expectation file** (`test/expectations/user-created.json`):\n\n```json\n{\n  \"tables\": {\n    \"Users\": {\n      \"count\": 1,\n      \"rows\": [\n        {\n          \"Email\": \"alice@example.com\",\n          \"Name\": \"Alice Example\",\n          \"Status\": 1\n        }\n      ]\n    }\n  }\n}\n```\n\n**Example with multiple tables** (`test/expectations/product-inventory.json`):\n\n```json\n{\n  \"tables\": {\n    \"Products\": {\n      \"count\": 1,\n      \"rows\": [\n        {\n          \"Name\": \"Example Product\",\n          \"Price\": 1999,\n          \"IsActive\": true\n        }\n      ]\n    },\n    \"Inventory\": {\n      \"count\": 1,\n      \"rows\": [\n        {\n          \"ProductID\": \"product-001\",\n          \"Quantity\": 0,\n          \"LastUpdated\": \"2024-01-01T00:00:00Z\"\n        }\n      ]\n    }\n  }\n}\n```\n\nThis pattern allows you to:\n\n- Verify UI actions resulted in correct database changes\n- Validate complex multi-table relationships\n- Catch data integrity issues early in the development cycle\n- Keep test expectations readable and version-controlled\n\n### Supported value types\n\n`spanner-assert` compares column values using `string`, `number`, `boolean`, `null`, and **arrays** of these primitive types.\n\n**Primitive types:**\n\n- `string`, `number`, `boolean`, `null`\n- For Spanner types like `TIMESTAMP` or `DATE`, provide values as strings (e.g., `\"2024-01-01T00:00:00Z\"`)\n\n**Array types (ARRAY columns):**\n\n- `ARRAY\u003cSTRING\u003e`, `ARRAY\u003cINT64\u003e`, `ARRAY\u003cBOOL\u003e` are supported\n- Arrays are compared with **order-sensitive matching** (exact element order required)\n- Empty arrays (`[]`) are supported\n\n**Array example:**\n\n```json\n{\n  \"tables\": {\n    \"Articles\": {\n      \"rows\": [\n        {\n          \"ArticleID\": \"article-001\",\n          \"Tags\": [\"javascript\", \"typescript\", \"node\"],\n          \"Scores\": [100, 200, 300],\n          \"Flags\": [true, false, true]\n        },\n        {\n          \"ArticleID\": \"article-002\",\n          \"Tags\": [],\n          \"Scores\": [],\n          \"Flags\": []\n        }\n      ]\n    }\n  }\n}\n```\n\n## Connection Management\n\nEach `assert()` call automatically manages its own database connection lifecycle:\n\n- **Auto-creation**: A fresh connection is created when you call `assert()`\n- **Auto-cleanup**: The connection is automatically closed when the assertion completes\n- **No manual management**: No need to call `close()` or manage connection lifecycle\n\nThis design keeps the API simple and prevents connection leak issues. You can call `assert()` multiple times without worrying about resource management:\n\n```ts\nconst spannerAssert = createSpannerAssert({\n  connection: {\n    projectId: \"your-project-id\",\n    instanceId: \"your-instance-id\",\n    databaseId: \"your-database\",\n    emulatorHost: \"127.0.0.1:9010\",\n  }\n});\n\n// Each call creates and closes its own connection\ntest(\"first assertion\", async () =\u003e {\n  await spannerAssert.assert(expectations1);\n});\n\ntest(\"second assertion\", async () =\u003e {\n  await spannerAssert.assert(expectations2);\n});\n```\n\n## Database Cleanup with `resetDatabase()`\n\nThe `resetDatabase()` method deletes all data from specified tables, making it useful for cleaning up test data between tests. This ensures each test starts with a clean slate.\n\n### Basic Usage\n\n```ts\nawait spannerAssert.resetDatabase([\"Users\", \"Products\", \"Orders\"]);\n```\n\n### Playwright Example\n\nUse `test.afterEach()` to automatically clean up after each test:\n\n```ts\nimport { test } from \"@playwright/test\";\nimport { createSpannerAssert } from \"spanner-assert\";\n\nconst spannerAssert = createSpannerAssert({\n  connection: {\n    projectId: \"your-project-id\",\n    instanceId: \"your-instance-id\",\n    databaseId: \"your-database\",\n    emulatorHost: \"127.0.0.1:9010\",\n  },\n});\n\ntest.describe(\"User Tests\", () =\u003e {\n  test.afterEach(async () =\u003e {\n    // Clean up database after each test\n    await spannerAssert.resetDatabase([\n      \"Users\",\n      \"Products\",\n      \"Orders\",\n    ]);\n  });\n\n  test(\"creates a new user\", async ({ page }) =\u003e {\n    // Test code...\n    await spannerAssert.assert(expectations);\n  });\n\n  test(\"updates user profile\", async ({ page }) =\u003e {\n    // Test code...\n    await spannerAssert.assert(expectations);\n  });\n});\n```\n## Row Matching Algorithm\n\n`spanner-assert` uses a **greedy matching algorithm** to compare expected rows against actual database rows. Understanding this behavior helps you write effective test expectations.\n\n### How It Works\n\nThe algorithm processes expected rows sequentially:\n\n1. For each expected row, it searches through the remaining actual rows\n2. When a match is found, that actual row is **immediately consumed** (removed from the pool)\n3. The algorithm moves to the next expected row\n4. Any expected rows that don't find a match are reported as missing\n\n**Key characteristics:**\n\n- **Subset matching**: Only columns specified in the expected row are compared. Additional columns in the database are ignored.\n- **Greedy selection**: The first matching actual row is chosen—no backtracking or optimization.\n- **Order-dependent**: The order of expected rows can affect the outcome if expectations are ambiguous.\n\n### Example: Subset Matching\n\n```json\n// Expected (only 2 columns specified)\n{\n  \"tables\": {\n    \"Users\": {\n      \"rows\": [\n        {\n          \"Email\": \"alice@example.com\",\n          \"Status\": 1\n        }\n      ]\n    }\n  }\n}\n```\n\nThis will match a database row even if it has additional columns:\n\n```\nActual database row:\n{\n  UserID: \"user-001\",\n  Name: \"Alice Example\",\n  Email: \"alice@example.com\",  ✅ matches\n  Status: 1,                    ✅ matches\n  CreatedAt: \"2024-01-01T...\"  ⬜ ignored (not in expectation)\n}\n```\n\n### Best Practice: Use Unique Identifiers\n\nTo avoid ambiguity and ensure reliable matching, **always include unique columns** (like primary keys) in your expectations:\n\n```json\n// ✅ Good: Specific and unambiguous\n{\n  \"tables\": {\n    \"Users\": {\n      \"rows\": [\n        {\n          \"UserID\": \"user-001\",\n          \"Email\": \"alice@example.com\",\n          \"Status\": 1\n        },\n        {\n          \"UserID\": \"user-002\",\n          \"Email\": \"bob@example.com\",\n          \"Status\": 1\n        }\n      ]\n    }\n  }\n}\n\n// ❌ Risky: Ambiguous expectations\n{\n  \"tables\": {\n    \"Users\": {\n      \"rows\": [\n        { \"Status\": 1 },\n        { \"Status\": 1 }\n      ]\n    }\n  }\n}\n```\n\n### When Greedy Matching Can Fail\n\nConsider this scenario:\n\n```json\n// Actual database has 3 rows:\n// - { UserID: \"A\", Status: 1 }\n// - { UserID: \"B\", Status: 1 }\n// - { UserID: \"C\", Status: 2 }\n\n// Expectations:\n{\n  \"tables\": {\n    \"Users\": {\n      \"rows\": [\n        { \"Status\": 1 }, // ① Ambiguous - matches A or B\n        { \"UserID\": \"A\" } // ② Specific - needs A\n      ]\n    }\n  }\n}\n```\n\nThe greedy algorithm will:\n\n1. Process expectation ①, match it to the first row with `Status: 1` (e.g., row A), and consume row A\n2. Process expectation ②, look for `UserID: \"A\"`, but row A was already consumed\n3. **Result: Assertion fails** even though a valid assignment exists\n\n**Solution**: Make expectations specific:\n\n```json\n{\n  \"tables\": {\n    \"Users\": {\n      \"rows\": [\n        {\n          \"UserID\": \"A\",\n          \"Status\": 1\n        },\n        {\n          \"UserID\": \"B\",\n          \"Status\": 1\n        }\n      ]\n    }\n  }\n}\n```\n\n### Combining `count` and `rows`\n\nUse both for comprehensive validation:\n\n```json\n{\n  \"tables\": {\n    \"Users\": {\n      \"count\": 10,\n      \"rows\": [\n        {\n          \"UserID\": \"admin-001\",\n          \"Role\": \"admin\"\n        }\n      ]\n    }\n  }\n}\n```\n\nThis ensures:\n\n- Exactly 10 users exist (not 9 or 11)\n- At least one admin user with ID \"admin-001\" exists\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnu0ma%2Fspanner-assert","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnu0ma%2Fspanner-assert","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnu0ma%2Fspanner-assert/lists"}