Unit vs. Integration vs. End-to-End Testing: The $125 Million Bug Between Two Components
In September 1999, NASA's Mars Climate Orbiter reached Mars and fired its engine to enter orbit. It never called home. The investigation found that two pieces of software had each done their jobs. The spacecraft's flight software expected thruster impulse data in newton-seconds, as the interface specification required. The ground software that produced that data reported it in pound-force seconds instead, a factor of 4.45 off. Each side was internally consistent. The bug lived in the gap between them, and a check that the two sides used compatible units was never done. The result was a lost orbiter, commonly cited at $125 million.
That's the story of a defect that no test of one component alone is designed to catch, and it's the cleanest way to understand why software teams write three kinds of tests, not one. Unit tests check a piece in isolation. Integration tests check that pieces work together. End-to-end tests check that the whole system serves a real user journey. Google's testing team suggests a 70/20/10 split between them as a first guess, and a 2026 flaky-test roundup reports that Meta's end-to-end tests run at roughly 10% flakiness while unit tests on the same codebases stay under 1%. Each layer buys you something different, and each costs something different.
This guide breaks down what each type of test actually does, where each is blind, and how to decide the right mix without cargo-culting a ratio. If you're newer to pipelines, our CI/CD Pipelines roadmap shows where these tests run once you've written them.
This post originally appeared on the Ciphemic Academia blog.
The Short Version
- Unit tests check one small piece of logic (a function or class) in isolation. They're fast, cheap, and pinpoint failures precisely.
- Integration tests check that several pieces work together: your code with a real database, a queue, another service, or a third-party API. They catch problems at the boundaries.
- End-to-end (E2E) tests drive the whole system the way a user would, through the UI or public API, in a production-like environment. They give the most realistic confidence at the highest cost.
If you want one default: write many unit tests, a moderate number of integration tests at your system's boundaries, and a small set of end-to-end tests for your most critical user journeys. Treat Google's 70/20/10 as a starting guess, not a law.
What All Three Are Actually Trying to Solve
Before the differences, the shared goal:
- Catching regressions before users do, so a change in one place doesn't quietly break another
- Giving you confidence to change code, because refactoring without tests is gambling
- Fast feedback, since a failure found in seconds is cheaper than one found in production
- Documenting intended behavior, because a good test shows how something is supposed to work
- Supporting automated delivery, since pipelines are only as trustworthy as the tests gating them
The tension is always the same: the more realistic a test is, the slower, costlier, and flakier it tends to be. The three layers exist to balance that trade-off.
Unit Tests
What it actually is: a test of the smallest meaningful piece of logic, run in isolation, with anything slow or external (databases, networks, clocks) replaced by simple stand-ins. Unit tests answer one narrow question: given these inputs, does this function return the right output?
A small example (a unit conversion, the kind of function that sits at the heart of the Mars Climate Orbiter story):
def to_newton_seconds(value, unit):
if unit == "lbf_s":
return value * 4.44822
if unit == "n_s":
return value
raise ValueError(f"unknown unit: {unit}")
def test_pound_force_seconds_convert_to_newton_seconds():
assert round(to_newton_seconds(1.0, "lbf_s"), 2) == 4.45
def test_newton_seconds_pass_through_unchanged():
assert to_newton_seconds(2.5, "n_s") == 2.5
Where it shines:
- Speed: Google's testing team has said that even a tenth of a second is slow for a unit test, which is why thousands can run on every commit
- Reliability: with nothing external to go wrong, unit tests rarely fail for reasons unrelated to the code
- Precise failures: when one breaks, you know which function to look at
- Cheap to write and maintain when code is designed to be testable
Where it struggles:
- Blind to interaction bugs: the Mars Climate Orbiter's two components could each pass their own tests and still fail together
- Mocks can lie: if you replace a dependency with a stand-in that behaves differently from the real thing, the test passes while reality fails
- Hard on untestable code: tightly coupled code with hidden dependencies makes unit tests painful to write, and writing testable code is a skill in itself
Who this suits: every project, as the foundation. Business logic, calculations, validation rules, and data transformations belong here.
Integration Tests
What it actually is: a test that exercises several real components together across a boundary: your service with a real database, your code against a message queue, one service calling another, or your app against a third-party API's test environment. The goal is to verify that the pieces agree with each other, not just that each works alone.
A small example (an API endpoint plus a real test database):
def test_create_order_saves_to_database(client, db):
response = client.post("/orders", json={"sku": "A1", "qty": 2})
assert response.status_code == 201
assert db.query("SELECT qty FROM orders WHERE sku = 'A1'") == [(2,)]
Where it shines:
- Catches boundary mismatches: wrong units, wrong field names, wrong data types, and schema drift between components are exactly what this layer is for
- Tests real behavior of your infrastructure: SQL queries, transactions, queue delivery, and serialization behave differently against the real thing than against a mock
- A cheaper way to find interaction bugs than E2E: Google's own guidance asks why you'd write a large end-to-end test when a smaller, focused integration test finds the same bug
- Medium speed: slower than unit tests, much faster than driving the whole product
Where it struggles:
- More setup: databases, containers, and test data need to be provisioned and reset
- Harder to localize failures: a failure could be in either component or in the glue between them
- Slower feedback than unit tests, so teams tend to run them less often, such as per pull request rather than per keystroke
- Fuzzy definition: teams use the term differently, so agree internally on what "integration" means for you
Who this suits: any system with databases, queues, external APIs, or multiple services, which is nearly every real backend. Microservice teams often add contract tests here to check that services agree on message formats.
End-to-End Tests
What it actually is: a test that drives the complete system from the user's side, through a real browser or the public API, against a production-like environment with all the real pieces running. It verifies that a whole user journey works from start to finish.
A small example (a browser test with Playwright):
import { test, expect } from '@playwright/test';
test('a new user can sign up and reach the dashboard', async ({ page }) => {
await page.goto('https://staging.example.com/signup');
await page.fill('#email', 'test.user@example.com');
await page.fill('#password', 'a-long-test-password');
await page.click('button[type=submit]');
await expect(page.locator('h1')).toHaveText('Welcome');
});
Where it shines:
- Most realistic confidence: if the signup journey passes, the front end, API, database, and configuration are all wired together correctly
- Catches environment and wiring problems that lower-level tests can't see: missing configuration, a broken deploy, an incompatible release
- Protects your critical business paths: sign up, log in, checkout, and payment are worth verifying as a user would experience them
Where it struggles:
- Slow feedback: Google's guidance notes you have to wait for the whole product to be built, deployed, and then for the tests to run
- Flakiness: end-to-end tests touch timing, networks, and shared environments. One 2026 roundup reports roughly 10% flakiness for Meta's end-to-end tests against under 1% for its unit tests, and Google reported in 2016 that around 16% of its tests showed some level of flakiness
- Hard to debug: a failure tells you a journey broke, not where
- Expensive to maintain: UI changes break tests that have nothing to do with a real bug
Who this suits: every product with real users, but in small numbers: a handful of critical journeys, not an exhaustive tour of every feature.
Side-by-Side Comparison
| Unit | Integration | End-to-End | |
|---|---|---|---|
| Scope | One function or class | Several real components together | The whole system, as a user |
| Speed | Fastest | Medium | Slowest |
| Reliability | Highest | Medium | Lowest, most prone to flakiness |
| Pinpoints failures | Precisely | Roughly | Poorly |
| Best at catching | Logic errors | Boundary and contract mismatches | Wiring, config, and journey-level failures |
| Maintenance cost | Low | Medium | High |
| Runs | On every commit | Per pull request or merge | Before release or on a schedule |
| First-guess share of tests | About 70% | About 20% | About 10% |
| Example tools | pytest, JUnit, Jest | Testcontainers, Supertest, contract tests | Playwright, Cypress, Selenium |
How the Layers Fit Together
The classic picture is a pyramid: lots of cheap, fast tests at the bottom, fewer slow, expensive ones at the top.
/\
/E2E\ few: slow, realistic, critical journeys
/------\
/ Integr.\ some: medium, boundaries and contracts
/----------\
/ Unit \ many: fast, precise, logic
/--------------\
Take the Mars Climate Orbiter as a worked example of what each layer would ask. We can't know for certain what any one practice would have caught, but we can see the question each raises:
- Unit tests ask: does each conversion function return the right number for the unit it's given? Both sides could answer yes.
- An integration test asks: when the ground software hands this value to the navigation software, does the number mean the same thing on both sides? That's the question nobody checked.
- An end-to-end check asks: does the full trajectory, built from the real pieces together, behave as predicted?
The failure lived at the interface, which is where integration testing earns its place.
How This Fits a Career Path
- Backend and full-stack engineers: writing good unit and integration tests is a baseline expectation, and it's often how code reviewers judge your work
- QA, SDET, and test automation roles: you'll own the end-to-end layer and the strategy for keeping it small and trustworthy
- DevOps and platform engineers: you decide which tests gate which stage of the pipeline; see our CI/CD pipelines vs. manual deployment post for how that automation fits together
- Interviews: "how would you test this?" is a common question, and strong answers name all three layers and justify the mix for the specific system
How to Choose Without Overthinking It
- Start with unit tests for your logic. Calculations, validation, and transformations are cheap to cover and pay back constantly.
- Add integration tests at every boundary. Anywhere your code meets a database, a queue, a third-party API, or another team's service, test the real interaction at least once.
- Pick three to five critical journeys for end-to-end tests, such as signup, login, and checkout, and resist the urge to add more. Run them after deploys or on a schedule, not on every keystroke.
- Treat flaky tests as bugs. Quarantine them, fix the root cause, and never train the team to shrug off red builds.
- Adjust the ratio to your system. Many external integrations call for more integration tests. A UI-heavy product may justify more end-to-end coverage. Treat 70/20/10 as a starting point.
A note on honesty: the 70/20/10 split is described by Google as a first guess, and engineers disagree about how to measure the ratio at all. The flakiness figures come from industry roundups and vary a lot by organization, test type, and how "flaky" is defined, so treat them as evidence of a real pattern, not as numbers you'll see in your own pipeline. The Mars Climate Orbiter's $125 million figure is the commonly cited cost of the orbiter itself. Figures vary by source, and the broader Mars Surveyor '98 mission, including the lander, cost considerably more.
Common Mistakes When Learning to Test
- The inverted pyramid. A suite that's mostly slow end-to-end tests feels thorough but gives slow, flaky feedback. Push checks down to the cheapest layer that can catch the bug.
- Mocking everything. If every dependency is a stand-in, your unit tests can pass while the real integration is broken, which is the Mars Climate Orbiter pattern in miniature.
- Calling a test a unit test when it isn't. If it needs a network, a database, or the file system, it's an integration test, and it probably runs slower than you think.
- Chasing a coverage percentage. High coverage with weak assertions gives false comfort. Test behavior that matters.
- Testing implementation details. Tests tied to internal structure break on every refactor and teach the team to distrust them.
- Ignoring flaky tests. Once people assume a red build is "probably flaky," real failures slip through.
- Skipping integration tests for database code. SQL, transactions, and migrations behave differently against a real database than against a mock.
Frequently Asked Questions
Which type of test should a beginner write first?
Unit tests. They're quick to write, give instant feedback, and teach you to structure code in testable pieces. Add integration tests as soon as your code talks to a database or another service.
Is 70/20/10 a rule I have to follow?
No. Google describes it as a first guess. Systems with many integrations need more integration tests, and UI-heavy products may need more end-to-end tests. The shape matters more than the exact numbers: many cheap tests, fewer expensive ones.
What's the actual difference between integration and end-to-end tests?
Integration tests check that a few components work together across a specific boundary, often without a UI. End-to-end tests drive the entire system as a user would, through a real interface, in a production-like environment. End-to-end tests are broader, slower, and flakier.
How many end-to-end tests do I need?
Fewer than you think. Cover your most important user journeys, such as signup, login, and checkout, and add more only when a real production failure justifies it.
What is a flaky test, and what should I do about it?
A flaky test sometimes passes and sometimes fails without any code change, usually from timing, shared state, or environment issues. Quarantine it, find the root cause, and fix or delete it. Leaving flaky tests in the main suite teaches people to ignore failures.
How do interviewers evaluate this topic?
They usually give you a feature and ask how you'd test it. A strong answer names unit tests for the logic, integration tests for the database and external calls, and a small number of end-to-end tests for the key journey, and explains why you wouldn't test everything at the top layer.
Build a Test Suite You Can Trust
The Mars Climate Orbiter wasn't lost because anyone skipped testing their own piece. It was lost in the space between two pieces that each worked, which is the exact space integration tests exist to check. Start with fast unit tests, test every boundary for real, and keep your end-to-end tests few and meaningful. Our free CI/CD Pipelines roadmap walks you through running these tests automatically on every change, and you can explore the courses for the backend and DevOps foundation underneath it.
Top comments (0)