Testing
Helm's tests are vitest-based, run in a Workers-compatible pool. Goal: enough coverage that confidence-in-deploy is high; not so much that the test suite is a maintenance burden.
Test layout
test/
unit/
tax-bc.test.js # pure-function helpers
audit-chain.test.js
integration/
customers.test.js # endpoint + D1 round trips
service-tickets.test.js
smoke/
cron-daily.test.js # cron handler triggered end-to-end
Running
npm test # full suite
npm test -- --watch # watch mode
npm test -- unit/ # one folder
Unit tests
Pure functions in src/lib/:
// test/unit/tax-bc.test.js
import { describe, it, expect } from 'vitest';
import { taxForLine } from '../../src/lib/tax-bc.js';
describe('taxForLine', () => {
it('new bike has GST only, no PST', () => {
expect(taxForLine({ tax_category: 'bike_new' })).toEqual({ gst: 0.05, pst: 0 });
});
it('component bundled with bike is PST-exempt', () => {
expect(taxForLine({ tax_category: 'component', bundled_with_bike: true })).toEqual({ gst: 0.05, pst: 0 });
});
it('component sold standalone gets full tax', () => {
expect(taxForLine({ tax_category: 'component', bundled_with_bike: false })).toEqual({ gst: 0.05, pst: 0.07 });
});
});
Unit tests are fast (~ms). Run them on every save.
Integration tests
Endpoint + D1 in the Workers test pool:
// test/integration/customers.test.js
import { describe, it, expect, beforeEach } from 'vitest';
import { SELF, env } from 'cloudflare:test';
beforeEach(async () => {
// Reset DB
await env.DB.exec(`DELETE FROM customers;`);
});
describe('POST /api/customers', () => {
it('creates a customer + writes an audit row', async () => {
const res = await SELF.fetch('http://x/api/customers', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Cookie: 'helm_session=<test_token>' },
body: JSON.stringify({ first_name: 'Test', last_name: 'User', phone: '250-555-0000' }),
});
expect(res.status).toBe(201);
const body = await res.json();
expect(body.id).toBeDefined();
const audit = await env.DB.prepare('SELECT * FROM audit_events WHERE entity_type=?').bind('customers').first();
expect(audit.action).toBe('customer.created');
});
});
Integration tests use a real in-process D1 with the test schema. Setup is in vitest.config.ts + wrangler.test.jsonc.
Smoke tests
End-to-end against a wrangler dev --local server:
// test/smoke/cron-daily.test.js
// Invokes the cron handler and asserts side effects
These are slower (seconds per test). Run them in CI; rarely locally.
What we test
- Pure functions: yes, exhaustively
- Mutation endpoints: yes, with at least one happy-path + one blocker test each
- Permission gating: yes, one test per endpoint that requires auth
- Cron handlers: smoke test that they run end-to-end without error
- Idempotency: tests that prove a duplicate request is a no-op
- Audit chain: tests that a mutation writes the audit row + chain hash matches
What we don't test
- UI rendering: not in v0.1; manual smoke is acceptable for vanilla JS at this scale
- Stripe API behavior: we trust their tests; we test our adapter's contract with them via mocks
- Twilio API behavior: same
- Claude API behavior: same
- Cloudflare runtime behavior: D1, R2, KV behave as documented; not our test surface
Fixtures
test/fixtures/:
customers.json— sample customer rowstickets.json— sample ticketsstaff.json— Sys Admin + a few test staff
beforeEach resets DB and loads fixtures. Tests should be independent (no shared state across tests).
CI
GitHub Actions runs npm test on every push and PR. A failing test blocks the merge. The bible's npm run build runs too.
See also
- Slice development pattern
- Code style
- Tech stack summary — vitest choice