Skip to content

Module 5 — QA/QC & testing in depth

Estimated: 1–1.5 weeks · Syllabus · ← Module 4 · Next → Module 6

  • Why we test. Tests are executable specifications and a safety net for change. They let you refactor fearlessly and catch regressions the moment they appear.
  • The testing pyramid. Many fast, isolated unit tests; fewer integration tests (components together); fewest, slowest end-to-end tests. Inverting this (mostly E2E) gives slow, brittle suites.
  • Test behavior, not implementation. Tests written against the public interface survive refactors; tests coupled to internals break on every change. This is information hiding applied to tests.
  • Coverage and its limits. Coverage measures which lines ran, not whether behavior is correct. High coverage of shallow assertions is a false comfort; aim for meaningful assertions on important paths.
  • Test doubles. Stubs, mocks, fakes — how to isolate a unit from its dependencies, and the danger of over-mocking (testing your mocks instead of your code).
  • Property-based testing (fast-check in the JS/TS world). Instead of hand-picked examples, state a property that must hold for all inputs and let the tool generate adversarial cases. Powerful for a fee engine: “total is never negative,” “adding a line item never decreases the total,” “fee is invariant under reordering of line items.”
  • Static analysis as QA. Linters (ESLint), formatters (Prettier), and the TypeScript compiler itself (tsc --noEmit in strict mode) catch whole classes of defects before tests run. Formatting on save ends style debate.
  • Continuous Integration. Automated gates on every push: format check → lint → type-check → test. Nothing broken merges. CI is the enforcement mechanism for everything above.
  • Pre-commit hooks. The same gates locally, before a commit lands — fast feedback, fewer CI failures.

The distinction between verification (“did we build it right?” — testing, types) and validation (“did we build the right thing?” — specs, requirements). Also: why exhaustive testing is impossible (infinite input space) and how partitioning, boundary analysis, and properties give leverage over infinity.

On Project B (FeeForge), build the pyramid deliberately:

  • Unit (many, with Vitest): the fee engine — boundaries like a zero-hour line item, a 0% and a 100% contingency, empty proposals, rounding behavior on currency. This is where the pure-domain boundary pays off: these tests are fast and need no DB or network.
  • Property-based (fast-check): total never negative; total invariant under line-item reordering; adding a positive line item never lowers the total.
  • Integration: a proposal saved through Prisma/Drizzle round-trips correctly; the API route returns the computed fee.
  • E2E (few, Playwright): create a proposal in the UI → fill the fee model → see the computed total. Mock the Anthropic API call so tests are deterministic and free — a good lesson in test doubles.
  • Wire pre-commit hooks and a GitHub Actions CI running format → lint → tsc → test on every push.

☐ Green CI on every push, a test suite you trust, and a demonstrated refactor made safe by that suite.

(working notes go here)