Testing¶
Doable uses Vitest across every package and service. One command at the repo root runs the lot.
Quickstart¶
pnpm test # all packages, in parallel via Turbo
pnpm test --watch # interactive
pnpm type-check # tsc --noEmit, no tests, fast
pnpm lint # ESLint
CI runs pnpm install --frozen-lockfile && pnpm type-check && pnpm lint && pnpm test.
Where tests live¶
- Unit tests are co-located:
foo.test.tsnext tofoo.ts. - Integration / route tests for the API live under
services/api/src/routes/__tests__/. - WS protocol tests under
services/ws/src/__tests__/. - DB migration sanity checks under
packages/db/src/__tests__/. - E2E (rare; mostly happens in
doablechore/) — Playwright scripts.
Conventions¶
- One
describeper public function or HTTP route. - Use
it.eachfor table-driven tests. - Mock at the boundary (network, filesystem, time). Avoid mocking your own modules — refactor instead.
- Time:
vi.useFakeTimers()+vi.setSystemTime(new Date('2026-01-01')). - Snapshots: only for stable, human-meaningful outputs (rendered Markdown, JSON-Schema). Avoid for HTML.
Database tests¶
Two flavours:
- Pure SQL helpers — start a throwaway Postgres container in a global setup file (
vitest.setup.ts), apply migrations, hand each test a transaction that's rolled back at the end. - Logic that doesn't touch the DB — pass the query helpers as DI and provide a fake. Cleaner and faster.
Pattern 1 example:
import { startTestDb, withTransaction } from '@doable/db/test';
let db: Awaited<ReturnType<typeof startTestDb>>;
beforeAll(async () => { db = await startTestDb(); });
afterAll(async () => { await db.stop(); });
it('inserts a project', async () => {
await withTransaction(db.sql, async (tx) => {
const id = await createProject(tx, { name: 'demo', ownerId: 'u1' });
expect(await getProject(tx, id)).toMatchObject({ name: 'demo' });
});
});
API route tests¶
Hono lets you app.request('/path', { method: 'POST', body }) synchronously — no need to bind a port:
import { app } from '../../app.js';
it('GET /projects requires auth', async () => {
const res = await app.request('/projects');
expect(res.status).toBe(401);
});
it('GET /projects returns the user list', async () => {
const token = signTestJwt({ sub: 'u1' });
const res = await app.request('/projects', {
headers: { authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.projects).toBeInstanceOf(Array);
});
AI / Copilot tests¶
Don't hit a real LLM in CI. Use:
- A fixture stream of recorded SSE events (
fixtures/anthropic-hello.sse) replayed via a fakefetch. - The
MockProvidershipped inpackages/docore/src/__test_helpers__/mock-provider.ts— accepts a script of events and yields them in order.
For end-to-end engine tests, DoCoreEngine accepts an injected provider:
const engine = new DoCoreEngine({ provider: new MockProvider(events), ... });
const stream = engine.run({ prompt: 'hi' });
const out = await collect(stream);
expect(out.map(e => e.kind)).toEqual([
'assistant.message_delta',
'assistant.message',
'done',
]);
Sandboxing tests¶
dovaultships its own suite that runs each backend (Direct,Systemd,JobObject,WindowsHeap) gated by platform. CI runs the appropriate ones for each runner OS.- For local development on Linux, the
Directbackend is enough.
Frontend tests¶
apps/web uses Vitest + Testing Library. Component tests focus on behaviour, not pixel layout.
import { render, screen } from '@testing-library/react';
import { ProjectCard } from './project-card';
it('shows the project name', () => {
render(<ProjectCard project={{ id: '1', name: 'demo' }} />);
expect(screen.getByText('demo')).toBeInTheDocument();
});
For visual regressions, use the Playwright tests in doablechore/ — they run against a deployed staging instance, not in PR CI.
Coverage¶
Outputs to coverage/ per package. We don't enforce a threshold in CI yet, but PRs that significantly drop coverage will get review comments.
Debugging a failing test¶
# In the package's directory:
pnpm vitest run path/to/file.test.ts -t 'name of the it block'
pnpm vitest --inspect-brk # attach Chrome devtools / VS Code
VS Code: install the Vitest extension, then click "Debug" next to any it.
Test-data hygiene¶
- Use random UUIDs in test data; never hard-code secrets.
- Rolled-back transactions are preferred over manual cleanup.
- Don't share state across tests within a file unless it's a global expensive setup.
What not to test¶
- Auto-generated types.
- Passthrough wrappers (one-line functions that just call through).
- Implementation details that change frequently — test the public contract.