CodeOath
← All posts
Testing36 min total · 12 parts

Testing Fundamentals: Unit, Integration, and E2E Tests Done Right

Part 10 of 12 · ~3 min

Integration vs. End-to-End Testing

Unit tests deliberately isolate one piece and fake everything around it — which means, by design, they can't catch bugs that only show up when real pieces actually talk to each other.

Integration tests wire together two or more real collaborators — your code and a real (often local) database, two of your own services talking over a real interface — and check that the seams between them behave correctly. They catch problems a unit test structurally cannot see: a SQL query that's syntactically valid but returns the wrong rows against a real schema, a serialization format one side produces that the other side can't actually parse, a database constraint that rejects data your unit-tested validation logic thought was fine. Every individual piece can be perfectly unit-tested and the integration can still be broken, because the bug lives in the interface between them, not inside either piece.

End-to-end (E2E) tests go a level further and drive the entire assembled system the way a real user actually would — a real browser, clicking real buttons, against a real (if test) deployment of the whole stack, database and all. They catch what neither unit nor integration tests can: a CSS change that visually hides a button real users would click, a multi-step user flow that breaks at the third step even though each individual page works fine on its own, a misconfigured environment variable that only shows up when the whole system boots together for real.

The trade-off moving up this ladder is consistent and unavoidable:

UnitIntegrationEnd-to-end
SpeedMillisecondsTens of ms to a few secondsSeconds to minutes
RealismLow — most collaborators are fakedMedium — some real, some fakedHigh — the real system, close to real usage
Flakiness riskVery lowLow to moderateHighest — timing, network, environment all in play
Failure locates the bugVery preciselySomewhat — narrows to the seam, not the exact linePoorly — "something broke somewhere in this flow"
Typical count in a healthy suiteHundreds to thousandsDozens to low hundredsA dozen to a few dozen, covering critical paths

None of the three layers is a substitute for either of the others — they catch categorically different classes of bugs. A suite that's "100% unit tested" can still ship a product where two correctly-tested services can't actually talk to each other, or where the login button silently doesn't render on production's actual CSS. The pyramid shape from the first section is exactly this trade-off, made deliberate: lean on the cheap, fast, precise layer for the bulk of your coverage, and spend the more expensive layers deliberately, on the handful of things only they can verify — critical user flows and real cross-boundary interfaces, not exhaustive coverage of every case (that's what the unit layer is for).