← Back to all articles

The Testing Pyramid: Unit Tests Should Dominate

TestingBeginner

What the pyramid looks like

  • Base: unit tests: many, fast, stable; cover functions and modules;
  • Middle: integration: verify modules working together, DB and APIs;
  • Top: end-to-end: few but vital; cover key user paths.

Upside-down is painful

A pile of e2e tests is slow, flakes on timing or animation, and hard to localize. Push most assertions down to unit tests for faster feedback.

Two reminders

  • Don't chase 100% coverage: prioritize critical logic and edges; glue code matters less;
  • Tests must be maintainable: flaky tests are worse than none; assert behavior, not implementation details.

Test data and environment hygiene

Good assertions cannot save you if data and environments are chaotic:

  1. Each test builds its own data: use factories or fixtures rather than a shared seed database, or tests interfere and cannot run in parallel;
  2. Isolation strategy: build unit-test data in memory and give integration tests their own schema or instance, never a shared dev database;
  3. Inject the clock: make "now" an injectable dependency rather than a direct system call, or tests fail cyclically across month ends and DST;
  4. Clean-up and idempotency: tests should remove what they create and be rerunnable, so a mid-run failure does not poison the next attempt;
  5. Consistent environments: keep dependency versions and config templates aligned across local, CI and staging, drift is the top cause of "works locally, fails in CI".

Tests need observability too: a failure should print the input, expectation, actual result and request ID, not merely "assertion failed".

Making tests trusted

  1. Failures must be explainable: a failing test should immediately show whether the expectation, the environment or the code is wrong. "Assertion failed" alone trains people to ignore the signal.
  2. Delete permanently failing tests: long-skipped or known-failing cases teach the team to disregard results. Fix or remove them, recording the reason in your tracker.
  3. Control runtime: unit tests should finish in seconds; slower means heavy dependencies and they get pushed out of daily use until a pre-release run.
  4. No ordering dependence: tests should run individually and in parallel, order dependence makes failures hard to reproduce and blocks incremental runs.
  5. Tie regressions to defects: add a reproducing case for every bug fixed; over time the suite becomes a precise list of known issues.

The value of a suite equals whether the team will act on its results. Everything that improves readability, speed and stability serves that one condition.