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 (integration) / MemoryEntityRepo (unit, preferred)
App (app/)vitestcreateMockStore / createEmptyMockStore (SQLite WASM)
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

EntityRepository — MemoryEntityRepo (preferred for service tests)

Services migrated to the EntityRepository pattern (v0.54.0+) use MemoryEntityRepo instead of mockD1.sql:

typescript
import { MemoryEntityRepo } from "workers/src/storage/repository";

const repo = new MemoryEntityRepo<StudentRow>("student_id", columns);
repo.seed({ student_id: "s1", display_name: "Alice", status: "ACTIVE" });
const service = new StudentService(repo);

const result = await service.findById("s1");
expect(result?.display_name).toBe("Alice");

Benefits: no SQL-string mocking, deterministic, fast, scope-aware via constructor injection.

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";

const store = createMockStore();
vi.mock("../../storage", () => ({ getStore: () => store }));

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

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

createTestStore — integration / behavior tests

Use when testing storage-layer behavior or end-to-end flows. Returns a real SQLiteStore with vfs: "memory" — real SQLite, ephemeral.

typescript
import { getStore } from "../../storage";
import { SQLiteStore } from "../../storage/sqliteStore";

const testStore = new SQLiteStore({ vfs: "memory", dbName: "my-test" });
vi.mock("../../storage", async () => ({
  ...(await vi.importActual("../../storage")),
  getStore: () => testStore,
}));

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

When to use which

ScenarioHelper
Testing service validation logiccreateMockStore()
Testing query result handlingcreateMockStore()
Testing storage-layer behaviorSQLiteStore({ vfs: "memory" })
Testing migration applicationSQLiteStore({ vfs: "memory" })
Testing transaction rollbackSQLiteStore({ vfs: "memory" })

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.

2026 Notes

@cloudflare/vitest-pool-workers

Worker integration tests use @cloudflare/vitest-pool-workers, which runs each test file in an isolated workerd worker with real D1 bindings. Configure in workers/vitest.integration.config.ts:

typescript
import { defineConfig } from "vitest/config";
import { readD1Migrations } from "@cloudflare/vitest-pool-workers/config";

export default defineConfig({
  test: {
    include: ["src/__integration__/**/*.test.ts"],
    retry: 2,
  },
});

TC-ID pattern in test names

Integration and unit tests can use traceability IDs in names:

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

TC-IDs are extracted automatically by pnpm docs:rtm to generate the traceability matrix.

resetDatabase() in beforeEach

Integration tests that use D1 call applyD1Migrations() in beforeAll. To isolate data between tests, use resetDatabase() or create test-specific data with beforeEach.

Distributed under MIT License.