Skip to content

Testing Patterns ​

This document defines how to structure tests in the monorepo. An autonomous agent should follow these conventions.

Stack ​

LayerToolMock DB
Workers (workers/)vitestmockD1.ts (mock of D1Database)
App (app/)vitestfake-indexeddb (IndexedDB polyfill)
Schemas (packages/schemas/)vitestNone (pure schema test)
Permissions (packages/permissions/)vitestNone (pure logic test)

Single Command ​

bash
pnpm test   # Runs all monorepo tests

Workers: test structure ​

Mock D1 ​

Use mockD1.ts to simulate the database:

typescript
// workers/src/__tests__/my-test.test.ts
import { describe, it, expect } from "vitest";
import { mockD1 } from "./mockD1";
import { getDB } from "../db/d1";

describe("My module", () => {
  it("does something", async () => {
    const db = mockD1();
    // db.prepare(...).bind(...).all() works
    // db.prepare(...).bind(...).first() works
    // db.prepare(...).bind(...).run() works
  });
});

Conventions:

  • describe("module name", ...) — name of the file or tested function
  • it("verb in present tense: does something specific", ...) — behavior, not implementation
  • Handler tests: mock getDB() before calling the handler

Handler test example ​

typescript
import { describe, it, expect, vi } from "vitest";
import { mockD1 } from "./mockD1";
import { getDB } from "../db/d1";

// Replace getDB with mock before each test
vi.mock("../db/d1", () => ({
  getDB: vi.fn(),
}));

describe("handleListClasses", () => {
  it("returns list of active classes", async () => {
    const db = mockD1();
    db.prepare().all.mockResolvedValue({
      results: [
        {
          class_id: "1",
          name: "Berçário",
          age_min: null,
          age_max: null,
          status: "ACTIVE",
          created_at: "2026-01-01",
          updated_at: "2026-01-01",
        },
      ],
      success: true,
    });
    (getDB as any).mockReturnValue(db);

    const response = await handleListClasses(
      new Request("http://localhost"),
      null as any,
      null as any,
      "corr-id",
    );
    expect(response.status).toBe(200);
    const body = await response.json();
    expect(body.items).toHaveLength(1);
    expect(body.items[0].name).toBe("Berçário");
  });
});

Workers — Integration tests (pool-workers) ​

For integration tests that need real D1 (SQLite via Miniflare), use @cloudflare/vitest-pool-workers:

typescript
// vitest.integration.config.ts
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
export default defineConfig({
  plugins: [cloudflareTest({ wrangler: { configPath: "./wrangler.toml" } })],
  test: { include: ["src/**/__integration__/**/*.test.ts"] },
});

Tests live in workers/src/__integration__/ and run separately from unit tests (pnpm test:integration).

The existing mockD1.ts mock was updated in v0.22.0 to support batch() for testing atomic D1 operations.

App: test structure ​

Mock IndexedDB ​

Use fake-indexeddb (already configured in the app's vitest.config.ts):

typescript
// app/src/modules/myModule/__tests__/my-test.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import { resetDatabase } from "../../../db/db";

describe("My module", () => {
  beforeEach(async () => {
    await resetDatabase(); // clears fake IndexedDB
  });

  it("does something", async () => {
    // Fake IndexedDB is ready for use
    // Use Dexie repositories normally
  });
});

Tests with seed ​

typescript
import { seedDatabaseIfNeeded } from "../../../db/seed";

beforeEach(async () => {
  await resetDatabase();
  await seedDatabaseIfNeeded(); // populates demo data
});

Schemas: test structure ​

Pure Zod schema test — no mock:

typescript
import { describe, it, expect } from "vitest";
import { studentCreateSchema } from "../../workers/src/schemas";

describe("studentCreateSchema", () => {
  it("accepts valid payload", () => {
    const result = studentCreateSchema.parse({
      displayName: "João",
      photoRef: "photo:ref",
      guardianName: "Maria",
      phones: [{ number: "11999999999", qualifier: "Celular" }],
      classId: "550e8400-e29b-41d4-a716-446655440000",
    });
    expect(result.displayName).toBe("João");
  });

  it("rejects empty name", () => {
    expect(() => studentCreateSchema.parse({ displayName: "" })).toThrow();
  });
});

Test Helpers — Mocking getStore() ​

(app/src/storage/test-helpers.ts)

Two strategies for mocking the storage layer in tests:

createMockStore — fast unit tests ​

Use when testing service logic in isolation. Returns a plain mock — no WASM, no schema, instant.

typescript
import { createMockStore } from "../../storage/test-helpers";
import { initMyServiceStore } from "../../modules/myService";

const store = createMockStore();
initMyServiceStore(store);

// Seed mock data
store.query.mockResolvedValue([{ id: "1", name: "Test" }]);

// Test the service
const result = await myService.doSomething();
expect(result).toHaveLength(1);

For service-module wiring, always follow the same store-accessor pattern as production: call the module's init*Store(store) instead of writing a vi.mock block on the storage module. There is no getStore() export to mock anymore — the seam is the accessor.

createTestStore — integration / behavior tests ​

Use when testing storage-layer behavior or end-to-end flows. createTestStore() returns a real DexieAdapter backed by fake-indexeddb — ephemeral IndexedDB, no browser needed.

typescript
import { createTestStore } from "../../storage";
import { initMyServiceStore } from "../../modules/myService";

const testStore = await createTestStore("my-test");
initMyServiceStore(testStore);

// Real IndexedDB operations — Dexie schema auto-initializes (DEXIE_SCHEMA)
await testStore.exec("INSERT INTO students (...) VALUES (...)");
const rows = await testStore.query("SELECT * FROM students");
expect(rows).toHaveLength(1);

For service-module wiring, follow the same store-accessor pattern as production: call the module's init*Store(testStore) instead of mocking getStore.

When to use which ​

ScenarioHelper
Testing service validation logiccreateMockStore()
Testing query result handlingcreateMockStore()
Testing storage-layer behaviorcreateTestStore() (DexieAdapter + fake-indexeddb)
Testing transaction rollbackcreateTestStore()
Testing schema/queriescreateTestStore()

Test ID Naming ​

Use the prefixes defined in the Test Execution Handbook:

PrefixDomain
TC-RBAC-###Role-based access control
TC-ATT-###Attendance
TC-STU-###Students
TC-SYNC-###Sync
TC-CONFLICT-###Conflict resolution
TC-TTL-###Session TTL
TC-A11Y-###Accessibility
TC-SEC-###Security
TC-COMP-###Compliance
TC-PERF-###Performance

Run before commit ​

bash
pnpm test   # Required. Husky's pre-commit hook also runs this.

Notas de 2026 ​

@cloudflare/vitest-pool-workers ​

Os testes de integracao do Worker usam @cloudflare/vitest-pool-workers, que roda a suite inteira em um unico worker workerd com storage compartilhado e bindings D1 reais. Configure em workers/vitest.integration.config.ts:

typescript
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    include: ["src/__integration__/**/*.test.ts"],
    // #749: um workerd para a suite inteira; o isolamento por arquivo vem de
    // resetAndMigrate() em src/__integration__/helpers.ts
    isolate: false,
    fileParallelism: false,
    // 1 (era 2): a contencao de CPU que motivava o retry sumiu com um unico
    // workerd; fica 1 rede para jitter de boot no runner de CI.
    retry: 1,
  },
});

Padrão TC-ID nos nomes de teste ​

Testes de integracao e unidades podem usar IDs de rastreabilidade nos nomes:

typescript
it("TC-SEC-001: login with invalid credentials returns 401", async () => { ... });

Os TC-IDs nos nomes mantem rastreabilidade manual com docs/RTM.csv (a geracao automatica do RTM foi removida em #640).

resetAndMigrate() no beforeAll ​

Testes de integracao que usam D1 chamam resetAndMigrate() no beforeAll: ele aplica o conjunto de migracoes uma vez por execucao e restaura a baseline pos-migracao antes de cada arquivo (isolamento por arquivo, nao por teste). Crie dados especificos por teste com beforeEach.

Distribuído sob licença MIT.