Test Architecture: How Many of Each, and Why
Most teams know they should have unit tests, integration tests, and end-to-end tests. Fewer have a deliberate strategyfor how many of each, what each layer is allowed to assume, and what to do when the CI pipeline starts pushing past 20 minutes. The pyramid and the trophy give you that conversation in a shape β they aren't rules, they're defaults to start from.
- CI takes 35 minutes and nobody wants to add another test
- You have 400 E2E tests, 50 unit tests, and most prod bugs are still missed
- Coverage is 92% but refactors keep breaking unrelated tests
- Flaky tests are retried automatically and the retry rate is climbing
The architecture decisions you make about your test suite β distribution, isolation, parallelism, what to gate on β drive engineering velocity more than any specific testing framework choice.
The Pyramid (Cohn)
flowchart TB
E[E2E - few, slow, broad coverage]
I[Integration - some, medium, real adapters]
U[Unit - many, fast, narrow]
E --> I --> U
style E fill:#fdd
style I fill:#fed
style U fill:#dfd
Many fast unit tests at the bottom; fewer integration tests in the middle; a small set of E2E tests covering the most important user journeys at the top. Each level is roughly 10Γ the cost of the level below it.
The Trophy (Dodds)
Kent C. Dodds proposed a different shape for frontend-heavy apps: a small base of static analysis (TypeScript, ESLint), some unit tests, a large integration body (component tests with React Testing Library), and a thin layer of E2E. The idea: integration tests give the best confidence-per-second for UI work, and TypeScript catches an entire class of bugs that would otherwise need unit tests.
flowchart TB
E2[E2E]
INT[Integration - the largest layer]
UN[Unit]
SA[Static analysis]
E2 --> INT --> UN --> SA
style INT fill:#dfd
Which Shape Fits Which Project
| Project type | Likely shape |
|---|---|
| Domain-heavy backend (banking, billing, logistics) | Pyramid; pure-domain unit tests are gold |
| SPA / frontend | Trophy; component integration tests dominate |
| Microservices | Pyramid + heavy contract testing |
| Glue / orchestration code | Mostly integration; less unit value |
| Data pipelines | Pyramid + golden-output snapshot tests |
Coverage Thresholds That Don't Lie
Line coverage above ~80% has rapidly diminishing returns and starts incentivizing weak tests written for coverage's sake. A working policy:
- Critical paths (auth, payments, data integrity): high branch coverage plus mutation testing.
- Regular code: 70β80% line coverage is fine if assertions are meaningful.
- Glue / configuration / dead-simple code: don't enforce a number; review and move on.
Mutation testing (Stryker for JS/.NET, Pitest for Java) is the real coverage signal: it mutates code (changes> to >=, removes lines) and runs your tests. Mutants that survive expose weak assertions. Aim for high mutation score on critical paths.
Test Pyramid Math β A Pipeline Budget
Assume you have a 10-minute CI budget. Roughly:
- 5,000 unit tests Γ ~5 ms each = 25 s. Parallelized across 4 workers, ~10 s.
- 500 integration tests Γ ~200 ms each = 100 s. Parallelized, ~30 s.
- 30 E2E tests Γ ~5 s each = 150 s. Sharded across 4 browsers / workers, ~50 s.
- Build + dependency install + framework startup: ~3 minutes.
That's a 5-minute pipeline. The moment you have 300 E2E tests instead of 30, you're at 18 minutes. Treat the E2E count as the most expensive constraint in the suite.
Parallelism and Isolation
flowchart LR
PR[PR commit] --> CI[CI orchestrator]
CI --> W1[Worker 1: unit + lint]
CI --> W2[Worker 2: integration shard A]
CI --> W3[Worker 3: integration shard B]
CI --> W4[Worker 4: e2e shard A]
CI --> W5[Worker 5: e2e shard B]
W1 & W2 & W3 & W4 & W5 --> GATE[Merge gate]
Run independent layers in parallel. Within a layer, shard by test file or test ID. Each worker needs its own ephemeral resources β DB, cache, fixture data β to avoid cross-test interference.
Quality Gates
Decide what blocks the merge button:
| Layer | Block on? |
|---|---|
| TypeScript / lint / format | Yes, always |
| Unit tests | Yes |
| Integration tests | Yes |
| Contract tests + can-i-deploy | Yes (between services) |
| E2E smoke (3β10 tests) | Yes |
| E2E full suite | On main or nightly, with auto-revert |
| Visual regression | Soft signal; reviewed in PR |
| Performance benchmarks | Soft signal; alert on regressions |
Reducing Flake
- Quantify it. Track first-attempt pass rate per test. Quarantine tests below 95%.
- Eliminate sleeps; use proper waits and fake timers.
- Per-test isolation β fresh DB schema or aggressive truncation, fresh browser context.
- Pin dependencies and toolchain versions; container the runtime.
- One retry on CI for known cosmic-ray flakes; never retry locally β fix it.
CI Pipeline Optimizations
- Run cheapest gates first (lint, types) so PRs fail fast.
- Cache the package install across runs; restore the browser binaries Playwright needs.
- Detect affected modules (Nx affected, Turborepo, change detection) and only run downstream tests.
- Reuse a Docker layer cache; build images once, push by tag, run tests against the artifact.
- Move slow visual regression to a separate workflow that runs on a label or schedule.
The Health Dashboard
A test suite is infrastructure. Treat it like one:
- P50/P95 pipeline duration.
- Flake rate per test, per suite.
- Coverage trend over time (per service / module).
- Top 10 slowest tests β split or optimize them.
- Suite owners β every quarantined test has a person, every old quarantine has a deadline.
Pitfalls
Inverting the pyramid
Hundreds of E2E tests βbecause they cover everythingβ results in a 40-minute, 5%-flake suite that no one trusts. Push tests down the pyramid: most of what an E2E catches can be caught by integration tests at one-tenth the cost.
Coverage as a target
Goodhart's law. The moment a coverage number is a target, people write tests that touch lines without asserting anything. Use coverage as a debugging tool, not a KPI.
No owner for the pipeline
If βCI is slowβ is everyone's problem, it's no one's. Name a tooling/infra owner with an explicit SLO on pipeline duration and flake rate. It's a real role and a high-leverage one.

