Testing
Architecture enables the test pyramid. Clear layers produce correct tests for each layer. Without clear architecture, tests test the wrong things at the wrong level.
Backend: Integration Tests First
Integration tests provide the most confidence per test on the backend. They verify layers work together — repositories, use cases, domain logic — without the fragility of mocking everything.
- Unit tests for pure business logic in the domain layer.
- A backend with only unit tests has gaps at every boundary.
- Focus: unit > integration > e2e, but invest more in integration.
Frontend: E2E Tests First
Most frontend code is rendering and interaction. E2E tests provide more confidence than component unit tests for this.
- Unit tests only for business logic that lives client-side.
- E2E tests are heavy and slow. But for critical user flows — checkout, authentication, onboarding — they are the only tests that verify what users experience.
Testing in the AI Era
Tests shift from catching bugs to defining correctness. They become the contract between past and future changes.
- When AI generates code, passing tests with good coverage provide trust during review.
- Without tests, reviewing AI-generated code is guesswork.
- Quality moves earlier: define what correct means before AI writes code.
Fitness Functions: Tests for Structure
The testing pyramid has a gap. Linters catch style. Unit tests catch logic. Integration tests catch contracts. Nothing catches structural decay — boundary erosion, layer violations, coupling growth.
Fitness functions fill that gap. A fitness function is any automated check that protects an architectural property: module boundaries, layer direction, schema contracts, complexity thresholds.
- Architecture spans multiple dimensions (structure, contracts, data, performance, security, reliability, observability). Each needs its own fitness functions.
- Most teams only cover maintainability via linters. Everything else is unchecked.
- AI makes this critical — an agent can degrade architecture in an afternoon. Fitness functions prevent structural decay at machine speed.
- Use the ratchet pattern: clean up a module, add enforcement, never regress. Quality only goes up.
See fitness-functions for dimensions, scope, timing, and the ratchet pattern in detail.
Rules
- Match test scope to code scope. Private module → private tests. Domain → unit tests. Infrastructure → integration tests.
- Use
beforeEachoverbeforeAll. Shared state between tests creates flaky tests. - Use inline snapshots for complex outputs — visible without separate files.
- Test edge cases explicitly. Empty inputs, boundary values, error conditions.
- Verify in dev environment before release. Automated tests are necessary but not sufficient for user-facing changes.
- Question unexpected test modifications in code review.
Decision Criteria
When deciding what to test: focus on the boundaries. Unit tests for domain logic. Integration tests for layer interactions. E2E for critical user flows. If unsure whether to write a test, ask: “what breaks if this is wrong, and would I catch it otherwise?”
Skip tests for trivial getters, simple mappings, and framework boilerplate. Invest test time in business logic, edge cases, and error paths.
Anti-patterns
- Testing implementation details — tests that break when refactoring without behavior change.
beforeAllwith shared mutable state — creates flaky tests and hidden dependencies between test cases.- Only unit tests on the backend — misses every integration boundary.
- No tests and “it works on my machine” — unverifiable claims do not ship.
- Forcing types in tests — casting to satisfy TypeScript instead of using actual data. Use factories.