AlgoInsight

Free lesson · Frontend System Design

How much of each kind of test?

Medium9 min readfrontendsystem-design

Testing isn't one activity - it's several, each with different speed, cost, realism, and the kind of bug it catches. The question that organizes all of testing strategy is not "should we test?" but "what's the right mix of test types, and how much of each?" Write only the fastest, most isolated tests and you'll have a green build that misses every bug in how your pieces fit together. Write only the most realistic, end-to-end tests and you'll have a slow, flaky, expensive suite that takes forever and that people stop trusting. The art is the distribution, and the testing pyramid is the classic mental model for getting it right. This opening lesson of the cluster establishes that model (and its important frontend refinement, the "testing trophy"), the tradeoffs that give it its shape, and the guiding principle - test behavior, not implementation - that the rest of the cluster builds on.

The pyramid's core claim is about proportions: you should have many fast, cheap, isolated tests at the base, fewer medium-cost tests in the middle, and few slow, expensive, realistic tests at the top. The shape isn't arbitrary - it falls directly out of the tradeoffs between the test types, and getting the proportions wrong (especially the common inversion, too many slow tests and too few fast ones) produces a suite that's expensive to run and maintain while catching less. Let's build up the layers, then the tradeoff axis that explains the shape, then the frontend-specific adjustment.

The three layers

Unit tests - the base. A unit test exercises the smallest pieces in isolation: a single function, a single component, a custom hook, a reducer - with its dependencies mocked away so you're testing just that piece. Unit tests are fast (milliseconds), cheap to write, stable (isolated, so deterministic), and precise (a failure points right at the broken unit). You have many of them. Their weakness: they prove each piece works alone, but not that the pieces work together - and a large share of real bugs live precisely in how pieces integrate.

Integration tests - the middle. An integration test exercises multiple units working together: a component rendered with its children, hooks, and state; a component talking to a mocked API; a small feature or page. They're medium cost and speed - slower than unit tests but far faster than end-to-end - and they catch the bugs that unit tests miss: the ones in the seams between pieces. For frontend specifically, this is the layer where most valuable bugs are caught, for reasons we'll see.

End-to-end (E2E) tests - the top. An E2E test drives the whole application in a real browser through a real user journey: open the app, log in, complete a task, verify the result - exercising the entire stack the way an actual user does. They give the highest confidence (they prove the real thing works end to end) but at the highest cost: slow (seconds to minutes each), expensive (real infrastructure), and brittle (prone to flaky, nondeterministic failures). You have few of them, reserved for critical paths.

graph TD
    E2E["E2E - few<br/>full app, real browser, real journeys<br/>slow · expensive · brittle · highest confidence"] 
    Int["Integration - more<br/>units working together<br/>medium cost · frontend sweet spot"]
    Unit["Unit - many<br/>isolated functions/components<br/>fast · cheap · stable · precise"]
    Static["Static analysis - base<br/>types + lint · free, instant"]
    E2E --> Int --> Unit --> Static
    Axis["↑ up: slower, costlier, more brittle, MORE realism/confidence<br/>↓ down: faster, cheaper, more stable, LESS realism"]
This diagram shows the testing pyramid. At the wide base sit static analysis - types and linting, which are free and instant - and unit tests, which are many, fast, cheap, stable, and precise because they test isolated functions and components. In the middle are integration tests: fewer in number, medium cost, testing multiple units working together, and the sweet spot for frontend. At the narrow top are end-to-end tests: few in number, driving the full app in a real browser through real user journeys, giving the highest confidence but being slow, expensive, and brittle. The axis running up the pyramid captures the tradeoff: going up means slower, costlier, more brittle, but more realism and confidence; going down means faster, cheaper, more stable, but less realism.

The tradeoff axis that explains the shape

The pyramid's shape is a direct consequence of a single tradeoff axis running top to bottom:

Because fast/cheap/stable tests let you have many and run them constantly, while slow/expensive/brittle tests force you to have few, the economical distribution is many at the bottom, few at the top - the pyramid. You want the bulk of your coverage from the cheap, fast, stable layer, with the expensive realistic layer reserved for the few flows important enough to justify the cost. Inverting this - many E2E tests, few unit tests - produces the "ice-cream cone" anti-pattern: a top-heavy suite that's slow, flaky, and painful to maintain, where the build takes forever and failures are as likely to be flakes as real bugs. The pyramid shape is the efficient allocation of testing effort against the cost/confidence tradeoff.

The frontend refinement: the testing trophy

Here's the important nuance for frontend specifically. The classic pyramid comes from backend testing, where isolated unit tests of pure logic carry most of the value. But frontend is different: UI "units" tested in total isolation often miss the real bugs, because frontend bugs disproportionately live in integration - how a component works with its state, its children, its data fetching, the actual DOM, and user interactions. A component that passes its isolated unit test can still be broken in how it integrates. This has led to a frontend-favored model, Kent C. Dodds's "testing trophy", which reweights toward integration tests as the sweet spot - the layer giving the most confidence per cost for UI - with a base of static analysis (TypeScript types and linting, which catch a whole class of errors for free, instantly), a solid layer of unit tests, a heavy emphasis on integration tests, and a thin top of E2E.

The classic pyramid (backend-style) versus the frontend 'testing trophy,' which adds a static-analysis base and reweights toward integration tests - the layer that gives frontend the most confidence per cost.

The practical takeaway: for frontend, integration tests are where you invest most, because they best balance realism (catching the integration bugs that dominate UI) against cost (much cheaper and more stable than E2E). You still write unit tests (for logic, utilities, complex components) and a few E2E tests (for critical journeys), and you lean on static analysis as the free base - but the center of gravity is integration. Knowing both models - the classic pyramid and the frontend trophy's integration emphasis - and being able to explain why frontend shifts toward integration is a senior-level framing that goes beyond reciting "many unit, few E2E."

What to test where

The models translate into a simple allocation of what goes in which layer:

This allocation, weighted toward integration for the UI layers, is the concrete output of the pyramid/trophy thinking.

The principle underneath it all: test behavior, not implementation

One principle, introduced here and deepened in the next lesson, governs good tests at every layer: test behavior, not implementation. A test should verify what the code does (the observable behavior a user or caller experiences) - not how it does it internally (specific internal state, private methods, CSS class names, the exact component structure). Tests coupled to implementation details are brittle: they break when you refactor even though nothing actually broke for users, and they fail to catch real bugs because they're checking the wrong thing. Tests coupled to behavior are resilient (you can refactor freely as long as behavior is preserved) and meaningful (they break when something users care about breaks). This principle is why frontend favors integration tests queried the way a user interacts (next lesson) and is the single most important idea for writing tests that help rather than hinder. Hold it from the start: good tests describe behavior; brittle tests describe internals.

Important: Testing strategy is about the mix: the testing pyramid prescribes many fast, cheap, stable tests at the base (unit) and few slow, expensive, brittle ones at the top (E2E), with integration between - a shape that follows the tradeoff axis (up = more realism/confidence but slower/costlier/flakier; down = faster/cheaper/stabler but less realistic). The common anti-pattern is the ice-cream cone (too many E2E, too few unit - slow and flaky). For frontend, the testing trophy refines this: a static-analysis base (types + lint), and a center of gravity on integration tests, because UI bugs disproportionately live in integration and isolated unit tests miss them. Allocate: logic → unit; component behavior/interactions → integration; critical journeys → E2E. The governing principle at every layer is test behavior, not implementation - for tests that survive refactors and catch real bugs.

Interview: Show you understand the mix and the frontend nuance: "I think in terms of the testing pyramid, but for frontend I lean toward the testing trophy - a base of static analysis with TypeScript and linting, plenty of unit tests for logic, but the center of gravity on integration tests, because most real UI bugs are in how components integrate with state, data, and the DOM, which isolated unit tests miss. E2E I keep few, for critical journeys like sign-up and checkout, because they're slow and flaky. I'd avoid the ice-cream-cone anti-pattern of too many E2E tests. And across every layer I test behavior, not implementation - querying the way a user interacts and asserting on output, not internal state or class names - so tests survive refactors and actually catch bugs." Naming the trophy's integration emphasis, the allocation, and the behavior-not-implementation principle is the senior answer.

Why the distribution pays off: the economics of testing

The pyramid/trophy isn't just an aesthetic preference - it's an economic argument, and seeing the economics makes the distribution feel inevitable rather than arbitrary. Two forces drive it. First, the cost of running tests scales with how high in the pyramid they sit: a unit test runs in milliseconds and thousands run in seconds, so you can run them on every keystroke; an E2E test runs in seconds-to-minutes and a few dozen already make a slow suite, so they can only run occasionally. If your coverage is concentrated at the top, your suite is too slow to run continuously, which defeats the purpose of fast feedback. Second, the cost of maintaining tests scales the same way: brittle, flaky E2E tests demand constant babysitting, while stable unit and integration tests largely tend themselves. A top-heavy suite is expensive on both axes - slow to run and costly to maintain - for less total bug-catching than a well-distributed suite. So the pyramid shape is simply the cost-efficient allocation: get the most confidence for the least running and maintenance cost.

This connects to the "shift left" principle that's recurred across this module (in security and accessibility): the earlier and cheaper you catch a bug, the better. A type error caught by the compiler (static analysis, the free base) costs nothing; a logic bug caught by a unit test in milliseconds costs little; an integration bug caught pre-merge costs more but is contained; a bug that escapes to production and is caught by a user costs the most (in user harm, debugging, and hotfixing). The testing distribution is a machine for catching bugs as early and cheaply as possible - which is why investing in the fast lower layers, where bugs are caught instantly and cheaply, pays off more than piling everything into slow, late E2E tests. The right pyramid is the one that maximizes bugs-caught-early per unit of testing cost.

Both cost/brittleness and realism/confidence rise as you move up the layers. The frontend sweet spot is integration - the band that maximizes confidence per unit of cost - which is why the trophy bulges there.

What you should carry out of this lesson

Testing strategy is fundamentally about the right mix of test types. The testing pyramid prescribes many fast, cheap, stable unit tests at the base and few slow, expensive, brittle E2E tests at the top, with integration between - a shape dictated by the tradeoff that realism and confidence rise as speed, cost-efficiency, and stability fall. The common failure is the inverted ice-cream cone (too many E2E). For frontend, the testing trophy refines the model: a static-analysis base (types + lint) and a center of gravity on integration tests, because UI bugs disproportionately live in integration that isolated unit tests miss. Allocate logic to unit tests, component behavior and interactions to integration tests, and critical journeys to E2E. The principle governing good tests at every layer is test behavior, not implementation - which keeps tests resilient to refactoring and focused on what actually matters to users.

With the model in place, the next lesson goes deep on the two layers where frontend invests most - unit and integration testing - including the tools (Jest/Vitest, React Testing Library, MSW) and a thorough treatment of the test-behavior-not-implementation principle that makes those tests worth writing.

Back to Frontend System Design