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.

Advanced · 180–240 minutesseed/reset · deterministic IDs · clock injection · parallel isolation · cacheNode 22+ · Firebase CLI course baseline 15.30.0 · JS SDK 12.19.0 · Admin SDK 14.4.0 · rules-unit-testing 5.0.2Mandatory lab demo project + Emulator Suite/no-cost · managed staging/production canaries explicitly optionalLast reviewed: 17 September 2026

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.

Learning outcomes
  • 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.
Execution and safety note

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.

Chapter 26 reproducibility baseline · reviewed 17 September 2026

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.
scripts/seed.mjs
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}));
export baseline
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.

scripts/reset-firestore.mjs
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.

clock.mjs
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

test-id.mjs
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.

What this proves

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

  1. Create atlasmart-v26-1 seed with three products, two tenants and one order.
  2. Write a test that intentionally uses Date.now() and a shared document; run it in parallel until it demonstrates nondeterminism.
  3. Repair with injected clock and per-worker identity or serial reset.
  4. Run the suite three times from emulators:exec --import; require identical test counts and migration-version distribution.
  5. At teardown, assert no leaked listener handles and call cleanup.
ci-repeat.sh
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

  1. Why must reset completion be awaited?
  2. When is an emulator export preferable to readable fixture code?
  3. Why inject a clock instead of mocking Firestore server timestamps everywhere?
  4. Why can cached data invalidate reset-based tests?
  5. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.