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

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.

Notas de 2026

@cloudflare/vitest-pool-workers

Os testes de integracao do Worker usam @cloudflare/vitest-pool-workers, que executa cada arquivo de teste em um worker workerd isolado com bindings D1 reais. Configure em 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,
  },
});

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 sao extraidos automaticamente pelo pnpm docs:rtm para gerar a matriz de rastreabilidade.

resetDatabase() no beforeEach

Testes de integracao que usam D1 chamam applyD1Migrations() no beforeAll. Para isolar dados entre testes, use resetDatabase() ou crie dados especificos por teste com beforeEach.

Distribuído sob licença MIT.