Skip to main content

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.

Drafted from planning · v0.1

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 rows
  • tickets.json — sample tickets
  • staff.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