Chapter 26 · Testing, Emulator Suite, CI/CD, Index/Rules Deployment, and Schema Migration
Seed / Reset Test Data, Deterministic IDs / Clock Handling, and Parallel Test Isolation
Make Firestore tests repeatable with versioned seed state, awaited reset, injected clocks, deterministic identities, cache control, and parallel isolation.
1. AtlasMart flaky test: yesterday’s data is today’s bug
Two CI workers both use products/sku-1. One test
advances an expiry field using the wall clock; another assumes
the default seed still exists. Depending on test order and
machine timing, the suite alternates between green and red. This
is not a Firestore correctness problem—it is a test-isolation
problem.
- Build seed state that is explicit, versioned and importable.
- Reset Firestore/Auth state between tests and verify reset completion.
- Inject a deterministic clock and deterministic IDs instead of depending on wall-clock timing or random IDs.
- Choose parallel-isolation strategies that avoid cross-test documents, auth users and listeners.
- Prevent client offline caches from resurrecting stale emulator state.
Use the Emulator Suite, a Firebase demo project, or an isolated test project for destructive, security-sensitive, billing-sensitive, migration, backup/restore, or write-heavy exercises unless the lesson explicitly marks managed verification as required. Treat shown output as expected evidence unless it is explicitly identified as captured output, and re-check current Firebase/Google Cloud edition, mode, quota, pricing, and security documentation before production execution.
AtlasMart keeps project ID
demo-atlasmart-firestore, Standard-edition Native
mode, database (default), Node.js 22+, Firebase
CLI course baseline 15.30.0, Firebase JavaScript
SDK 12.19.0, Firebase Admin Node SDK
14.4.0,
@firebase/rules-unit-testing 5.0.2, Firestore
emulator 127.0.0.1:8080, Auth emulator
127.0.0.1:9099, and Emulator UI
127.0.0.1:4000. Mandatory work uses a
demo- project and local emulators only. Cloud
deployment, IAM, App Check enforcement, billing, production
indexes, quotas, contention, Query Insights/Key Visualizer,
backup/PITR, and Enterprise/MongoDB-compatibility canaries are
explicitly optional managed checks, never silently inferred
from emulator success.
2. Four sources of nondeterminism
| Source | Bad symptom | Deterministic control |
|---|---|---|
| Mutable emulator state | Test passes only after emulator restart. | Clear database per test or import a versioned baseline at suite start. |
| Wall clock | TTL/session/migration boundary flakes near second/minute boundaries. |
Inject now() / fixed epoch into domain
code.
|
| Random IDs | Assertions cannot predict paths; parallel workers collide unpredictably. | Use deterministic IDs derived from fixture + worker namespace; reserve auto IDs for tests that specifically validate them. |
| Shared auth/cache | Wrong tenant/claims or stale document appears after reset. | Create explicit auth contexts; disable/clear local persistence in emulator tests. |
3. Seed state: source-controlled intent vs binary emulator snapshot
Use two seed forms for different jobs:
| Seed form | Strength | Risk | Use |
|---|---|---|---|
| Readable fixture code/JSON | Reviewable semantic intent; easy schema diffs. | Slower for a large dataset. | Unit/integration fixtures and migration tests. |
| Emulator export | Fast shared snapshot for Firestore/Auth/Storage emulators. | Opaque-ish generated files can hide semantic drift. | Large stable baseline; regenerate from reviewed fixture source. |
import { db } from "../src/admin-emulator.mjs";export const FIXED_NOW = new Date("2026-09-17T09:00:00.000Z");const products = [ ["t-red__p-001", { tenantId:"t-red", name:"Red Mug", active:true, schemaVersion:1 }], ["t-red__p-002", { tenantId:"t-red", name:"Steel Bottle", active:true, schemaVersion:1 }], ["t-blue__p-001", { tenantId:"t-blue", name:"Blue Mug", active:true, schemaVersion:1 }]];for (const [id, data] of products) { await db.doc(`products/${id}`).set({ ...data, updatedAt: FIXED_NOW });}console.log(JSON.stringify({seedVersion:"atlasmart-v26-1", products:products.length}));
firebase emulators:start --project demo-atlasmart-firestore --only auth,firestore# in another terminal after reviewed seed has run:firebase emulators:export ./test/seed/atlasmart-v26 --force# CI can later use:firebase emulators:exec --project demo-atlasmart-firestore \ --only auth,firestore --import=./test/seed/atlasmart-v26 \ "npm test"
4. Reset must be awaited
The Firestore emulator exposes a test-only REST delete endpoint. A suite that fires the DELETE and immediately seeds can race its own cleanup. Await completion, then assert the database is empty or the seed version matches.
const project = "demo-atlasmart-firestore";const url = `http://127.0.0.1:8080/emulator/v1/projects/${project}/databases/(default)/documents`;const res = await fetch(url, { method: "DELETE" });if (!res.ok) throw new Error(`reset failed: ${res.status} ${await res.text()}`);console.log("firestore reset complete");
For Rules tests,
RulesTestEnvironment.clearFirestore() is usually
cleaner. Use the REST endpoint for broader integration harnesses
that are not built around rules-unit-testing.
5. Clock handling: Firestore server timestamps are observable but not injectable
Domain decisions such as “is session expired?” should not be
hidden behind direct calls to Date.now() inside
transaction callbacks. Pass a clock into the application layer.
Where you specifically need to test
serverTimestamp(), assert shape/order properties
rather than exact wall-clock equality.
export const realClock = { now: () => new Date() };export const fixedClock = iso => ({ now: () => new Date(iso) });export function nextExpiry(clock, ttlMs) { return new Date(clock.now().getTime() + ttlMs);}// deterministic testconst clock = fixedClock("2026-09-17T09:00:00.000Z");console.assert(nextExpiry(clock, 60_000).toISOString() === "2026-09-17T09:01:00.000Z");
6. Deterministic IDs: identity semantics must survive migration tests
import { createHash } from "node:crypto";export function testId(worker, logicalKey) { const h = createHash("sha256").update(`${worker}:${logicalKey}`).digest("hex").slice(0, 12); return `${worker}__${h}`;}console.log(testId(process.env.TEST_WORKER_ID ?? "w0", "cart:u-17"));
Do not globally replace production auto IDs with semantic IDs just to simplify tests. Keep production ID strategy intact and inject deterministic IDs only at the repository/test seam where the domain permits it.
7. Parallel isolation strategies
| Strategy | Isolation strength | Tradeoff |
|---|---|---|
| One emulator process per CI worker on distinct ports/project IDs | Strongest process isolation. | More startup cost and port management. |
| One emulator, per-worker collection/document namespace | Good for application tests. | Rules/query behavior must match real collection paths; prefixes can accidentally alter model. |
| One emulator, serial tests + full reset | Simple and faithful paths. | Longer wall-clock time. |
| Shared mutable state | None. | Do not use for correctness tests. |
Rules tests usually benefit from real collection paths plus
serial or explicit reset. Large application suites can shard by
separate emulator process/project ID, e.g.
demo-atlasmart-firestore-w1, w2.
8. Offline cache trap
The Firestore emulator clears server-side state on shutdown/reset, but a mobile/web SDK cache can outlive the emulator state. In emulator integration tests, disable persistent cache or create fresh app instances and clear test persistence. Otherwise an assertion can observe cached data that no longer exists locally.
A clean-state test proves behavior from a declared fixture and clock. It does not prove production concurrency, cache eviction policy under real mobile lifecycle, or managed-service latency.
9. Mandatory lab: make failure reproducible
-
Create
atlasmart-v26-1seed with three products, two tenants and one order. -
Write a test that intentionally uses
Date.now()and a shared document; run it in parallel until it demonstrates nondeterminism. - Repair with injected clock and per-worker identity or serial reset.
-
Run the suite three times from
emulators:exec --import; require identical test counts and migration-version distribution. - At teardown, assert no leaked listener handles and call cleanup.
set -euo pipefailfor i in 1 2 3; do echo "=== deterministic pass $i ===" firebase emulators:exec --project demo-atlasmart-firestore \ --only auth,firestore --import=./test/seed/atlasmart-v26 \ "npm run test:rules && npm run test:integration"done
10. Wrong approach: “random data makes tests realistic”
Unseeded randomness can expand coverage, but without a captured seed it destroys reproducibility. Property-based/fuzz tests should log the PRNG seed and minimal failing case. Deterministic integration fixtures should remain deterministic. Randomness is a test technique—not a substitute for fixture design.
Production judgment and bridge
Parallelism is valuable only if it does not change the model under test. Favor isolation you can explain and audit. Lesson 3 now versions the two other pieces that often drift outside CI: Security Rules and index definitions.
Knowledge check
- Why must reset completion be awaited?
- When is an emulator export preferable to readable fixture code?
- Why inject a clock instead of mocking Firestore server timestamps everywhere?
- Why can cached data invalidate reset-based tests?
- What is the strongest parallel isolation?
Review the answers
1. Otherwise seeding can race cleanup and produce intermittent missing/extra documents.
2. For a large stable baseline where fast startup matters; it should still be generated from reviewed semantic fixtures.
3. Business-time decisions become deterministic without pretending the server clock itself is controllable.
4. The client cache can retain data after emulator state is cleared, so a read may not represent the new server baseline.
5. Separate emulator process/project ID/ports per worker, at the cost of more setup.
Summary and next step
This lesson established the working contract for Seed/Reset Test Data, Deterministic IDs/Clock Handling, and Parallel Test Isolation. Keep its edition/mode assumptions, trust boundary, verification evidence, and operational constraints explicit when reusing the pattern.
Next, continue to Version Security Rules and Index Definitions, CI Validation, Staged Deployment, and Rollback.
Authoritative references
- Firebase: Connect your app to the Cloud Firestore Emulator — demo projects, edition selection, Admin SDK environment variables, reset/import/export, Rules reports, and production differences.
-
Firebase: Install, configure and integrate Local Emulator
Suite
—
emulators:exec, import/export and CI workflow. - Firebase: Test Cloud Firestore Security Rules — emulator rules testing and CI execution.
-
Firebase: Build Security Rules unit tests
—
@firebase/rules-unit-testing, mocked auth, clearing state and disabled-rules fixture setup. - Firebase: Manage and deploy Security Rules — source control, local testing and selective deployment.
- Firebase: Manage indexes in Cloud Firestore — source-controlled index definitions and CLI deployment.
- Firebase: Cloud Firestore index definition reference — composite, field override and vector index JSON shapes.
- Firebase CLI reference — configured multi-database rules/indexes and deployment selectors.