Testing Patterns
This document defines how to structure tests in the monorepo. An autonomous agent should follow these conventions.
Stack
| Layer | Tool | Mock DB |
|---|---|---|
Workers (workers/) | vitest | mockD1.ts (mock of D1Database) |
App (app/) | vitest | fake-indexeddb (IndexedDB polyfill) |
Schemas (packages/schemas/) | vitest | None (pure schema test) |
Permissions (packages/permissions/) | vitest | None (pure logic test) |
Single Command
pnpm test # Runs all monorepo testsWorkers: test structure
Mock D1
Use mockD1.ts to simulate the database:
// 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 functionit("verb in present tense: does something specific", ...)— behavior, not implementation- Handler tests: mock
getDB()before calling the handler
Handler test example
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:
// 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):
// 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
import { seedDatabaseIfNeeded } from "../../../db/seed";
beforeEach(async () => {
await resetDatabase();
await seedDatabaseIfNeeded(); // populates demo data
});Schemas: test structure
Pure Zod schema test — no mock:
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.
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.
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
| Scenario | Helper |
|---|---|
| Testing service validation logic | createMockStore() |
| Testing query result handling | createMockStore() |
| Testing storage-layer behavior | createTestStore() (DexieAdapter + fake-indexeddb) |
| Testing transaction rollback | createTestStore() |
| Testing schema/queries | createTestStore() |
Test ID Naming
Use the prefixes defined in the Test Execution Handbook:
| Prefix | Domain |
|---|---|
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
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:
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:
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.