← Back

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.

Rules

  • Match test scope to code scope. Private module → private tests. Domain → unit tests. Infrastructure → integration tests.
  • Use beforeEach over beforeAll. 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.
  • beforeAll with 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.