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";
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.
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
| Scenario | Helper |
|---|---|
| Testing service validation logic | createMockStore() |
| Testing query result handling | createMockStore() |
| Testing storage-layer behavior | SQLiteStore({ vfs: "memory" }) |
| Testing migration application | SQLiteStore({ vfs: "memory" }) |
| Testing transaction rollback | SQLiteStore({ vfs: "memory" }) |
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 executa cada arquivo de teste em um worker workerd isolado com bindings D1 reais. Configure em workers/vitest.integration.config.ts:
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:
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.