Software Development Trending

Unit vs Integration vs End-to-End Testing Explained

Unit, integration and end-to-end tests catch different bugs at different costs. Here is what each one should cover, with examples, and how to balance them using the test pyramid.

Quick answer

Unit tests check one small piece of code in isolation and run in milliseconds. Integration tests check that several pieces, such as your code and a database, work together. End-to-end tests drive the whole app the way a user would. Most teams write many unit tests, fewer integration tests and a small set of end-to-end tests.

Unit vs Integration vs End-to-End Testing Explained

Photo: Unsplash

On this page

Every team that ships software eventually hits the same wall: a bug reaches production even though “we have tests.” Usually the problem isn’t the number of tests. The tests are checking the wrong things at the wrong level. A pile of slow browser tests that break every week is as unhelpful as a thousand unit tests that never touch the database.

This guide explains the 3 main test types, what each should and shouldn’t cover, and how to balance them so your suite catches real bugs without slowing every release.

The short version

Unit test Integration test End-to-end (E2E) test
What it checks One function, class or component Several parts working together A full user journey through the real app
Typical scope No network, no database, no file system Real database, queue, API or framework wiring Browser or API client against a deployed stack
Speed Milliseconds Seconds to a few seconds Seconds to minutes each
How often it breaks for no reason Rarely Sometimes Most often (flaky)
What a failure tells you Exactly which logic is wrong Which connection or contract is wrong That something, somewhere, is broken
Example tools Jest, Vitest, pytest, JUnit Same runners plus Testcontainers, Supertest Playwright, Cypress, Selenium
How many to write Many Some Few

Key takeaway: Push each check down to the lowest level that can catch it. Use end-to-end tests only for the handful of journeys your business can’t afford to break.

Unit tests: fast checks on small pieces

A unit test calls one piece of code with known inputs and checks the output. The “unit” is usually a function, a class or a single UI component. Anything slow or unpredictable, like a database, network call or the system clock, is replaced with a fake.

Here is a simple example in JavaScript using Jest or Vitest:

```javascript // discount.js export function applyDiscount(total, code) { if (code === “SAVE10”) return Math.round(total * 0.9 * 100) / 100; return total; }

// discount.test.js import { applyDiscount } from “./discount”;

test(“SAVE10 takes 10% off”, () => { expect(applyDiscount(50, “SAVE10”)).toBe(45); });

test(“unknown codes change nothing”, () => { expect(applyDiscount(50, “BOGUS”)).toBe(50); }); ```

What unit tests should cover:

  • Business rules: pricing, tax, discounts, eligibility, permissions logic
  • Edge cases: empty lists, zero, negative numbers, very long strings, time zones
  • Error handling: what happens with bad input
  • Pure UI logic: a component shows the right label for each state

What they shouldn’t try to cover: whether your SQL query actually works, whether the payment provider accepts your request, or whether the login page loads in Safari. Mocking those in a unit test proves only that your mock behaves the way you told it to.

Unit tests are cheap enough to run on every save. That fast feedback is their main value, so keep them fast. If a “unit” test takes seconds, it’s probably doing integration work.

Integration tests: checking the connections

An integration test checks that 2 or more parts work together for real. Common examples:

  • Your repository code against a real database (often started in a container for the test run)
  • An HTTP endpoint, tested through your web framework, including routing, validation and serialization
  • A message published to a queue and picked up by the consumer
  • Your code calling a third-party API’s sandbox, or a contract test against its published schema

Here is the kind of thing an API integration test checks in plain terms: “POST /orders with a valid body returns 201, writes 1 row to the orders table and the row has the right total.” No browser is involved, but the real framework and database are.

What integration tests should cover:

  • Database queries, migrations and transactions
  • Serialization: dates, decimals, nulls and enums surviving the trip in and out
  • Authentication middleware actually blocking anonymous requests
  • Contracts between your services, which matters a lot once you move from a monolith to microservices

The trade-off: integration tests are slower and need setup, such as seeded data and cleanup between tests. In the widely cited Practical Test Pyramid article on martinfowler.com, Ham Vocke also warns that tests against a fake version of an external service can pass while the real service has changed. That is why contract tests, which check that a provider and its consumers agree on the interface, sit alongside integration tests in many teams.

End-to-end tests: the user’s point of view

An end-to-end test drives the whole system the way a user does. For a web app, that usually means a browser automation tool clicking through pages against a deployed test environment. Playwright’s documentation, for example, lists support for Chromium, WebKit and Firefox on Windows, Linux and macOS, and runs tests in parallel by default.

A typical E2E test reads like a user story: open the store, search for a product, add it to the cart, check out with a test card, and confirm the order page shows a number.

What E2E tests should cover:

  • The few journeys that make or lose money: sign up, log in, checkout, password reset
  • Cross-system flows nobody owns end to end, like order to email to fulfillment
  • Smoke tests after each deployment: “the app loads and a user can log in”

What they shouldn’t cover: every validation message, every discount rule or every permutation of a form. Those belong lower down. The Practical Test Pyramid article calls end-to-end tests notoriously flaky and expensive to maintain, and recommends keeping them to the most important user journeys.

Flakiness is the real cost. When an E2E test fails because a page loaded 200 milliseconds late, people learn to rerun and ignore failures. Then a real failure slips through.

The test pyramid and how to use it

The test pyramid, introduced by Mike Cohn in his book Succeeding with Agile, is a simple picture: a wide base of unit tests, a middle layer of integration or service tests and a narrow top of end-to-end tests. The higher you go, the fewer tests you should have.

A 2015 Google Testing Blog post offered roughly 70% unit, 20% integration and 10% end-to-end as a starting point, while noting the right mix varies by team. Treat that as a rule of thumb, not a target to hit.

The shape to avoid is the “ice cream cone”: a thin layer of unit tests, some integration tests and a huge stack of manual and browser tests on top. It feels thorough but runs slowly, fails randomly and gives vague error messages.

Some front-end teams prefer a “testing trophy” shape that puts more weight on integration-style component tests, because modern UI frameworks make those tests fast. That’s a reasonable variation. The principle stays the same: most of your confidence should come from tests that are fast and precise.

What each layer looks like for 1 feature

Take a “coupon code at checkout” feature:

Level Tests to write
Unit Valid code, expired code, code below minimum spend, stacking rules, rounding
Integration Coupon saved and read from the database, API rejects an expired code with a 400, usage count increments in a transaction
End-to-end 1 test: a shopper applies a valid code and the discounted total is charged

Notice that the E2E test doesn’t retest every rule. It proves the pieces are wired together.

Common mistakes that waste test effort

  • Mocking everything. If every dependency is mocked, your tests pass while production breaks. Use real dependencies in integration tests.
  • Testing implementation details. Tests that check private methods or exact HTML structure break on every refactor. Test behavior and outputs instead.
  • Duplicating checks across layers. If a unit test already proves a rule, the E2E test doesn’t need to.
  • Ignoring flaky tests. Quarantine and fix them quickly. A suite people don’t trust is a suite people skip.
  • Chasing a coverage number. Coverage shows what code ran, not whether the assertions were meaningful. Use it to find untested areas, not as a grade.
  • Running everything at the end. Slow suites that run only before release push bug discovery late, which is a classic source of technical debt.

Where to start: building a balanced suite

If your project has few tests, or the wrong mix, use these steps:

  1. List your critical journeys. Pick the 3 to 10 flows that would cost money or trust if they broke. Those are your E2E candidates.
  2. Add a smoke test first. One E2E test that loads the app and logs in catches a surprising number of broken deployments.
  3. Cover business rules with unit tests. Start with the code that changes often or has caused bugs before.
  4. Add integration tests at each boundary. Database, external APIs and service-to-service calls each get a few focused tests.
  5. Run tests in your pipeline. Unit tests on every commit, integration tests on every pull request, E2E tests before or right after each deployment. Our guide to building a CI/CD pipeline shows where each stage fits.
  6. When a bug escapes, write the lowest-level test that would have caught it. Over time this shapes the pyramid naturally.
  7. Review slow and flaky tests monthly. Delete or push down any high-level test that a lower-level test already covers.

Static type checking adds another cheap safety layer below unit tests. If you work in JavaScript, see TypeScript vs JavaScript for what types catch before a test even runs. And before a big release, pair your automated suite with a pre-launch QA checklist.

Conclusion

Unit, integration and end-to-end tests aren’t competitors. They answer different questions. Unit tests ask “is this logic right?”, integration tests ask “do these parts talk to each other correctly?” and end-to-end tests ask “can a user actually do the thing?” Write many of the first, a solid layer of the second and just enough of the third. Your suite will run faster, fail for real reasons and tell you exactly where to look. For more on how testing fits into delivery, read what CI/CD is or browse the glossary.

Sources

Questions readers ask

Frequently asked questions

What is the difference between unit and integration testing?

A unit test checks one small piece of code in isolation, with dependencies like databases replaced by fakes. An integration test checks that several real pieces work together, such as your code plus a real database or API. Unit tests are faster and pinpoint logic errors. Integration tests are slower but catch problems in queries, configuration, serialization and contracts that unit tests can't see.

Is end-to-end testing the same as integration testing?

No. Integration tests check a few connected parts, usually without a user interface, such as an API endpoint writing to a database. End-to-end tests run a complete user journey through the whole deployed system, often in a real browser. E2E tests give the most realistic confidence but are the slowest, most expensive to maintain and most likely to fail for reasons unrelated to real bugs.

How many unit tests vs integration tests should I have?

Most teams aim for many more unit tests than integration tests, and only a small number of end-to-end tests. A 2015 Google Testing Blog post suggested roughly 70/20/10 as a starting point. The exact ratio matters less than the principle: test each rule at the lowest level that can catch it, and reserve slow tests for the connections and journeys that need them.

What is the test pyramid?

The test pyramid is a model popularized by Mike Cohn in his book Succeeding with Agile. It shows a wide base of fast unit tests, a smaller middle layer of integration or service tests and a narrow top of end-to-end UI tests. The shape reminds teams that higher-level tests are slower and more fragile, so you should have fewer of them as you move up.

Do I need end-to-end tests if I have good unit tests?

Yes, but only a few. Unit tests can't prove that the front end, back end, database and third-party services are wired together correctly in a real environment. A small set of end-to-end tests for your most important journeys, such as sign-up, login and checkout, catches broken deployments and configuration mistakes that lower-level tests miss.

What are the best tools for end-to-end testing?

Widely used end-to-end testing tools include Playwright, Cypress and Selenium for web apps, plus platform tools for mobile apps. The right choice depends on your language, the browsers you must support and how your team writes tests. Pick one tool, keep the suite small and focused, and run it in your CI pipeline so failures surface before customers see them.

Why are end-to-end tests flaky?

End-to-end tests are flaky because they depend on many moving parts at once: network timing, page load speed, test data, third-party services and browser behavior. Any of these can cause a failure that has nothing to do with a real bug. Reduce flakiness by using auto-waiting tools, isolated test data, stable selectors and fewer E2E tests overall.

Reader reviews

Was this guide useful

No reviews yet. Share your experience and help other readers.

Write a review
Your rating
Never shown publicly

Reviews are checked before they appear. We never show your email. Please review only places or services you have used.

Manishdai
Written by

Manishdai

Manishdai writes about the technology that is reshaping every industry — from AI agents and cloud platforms to healthtech, fintech and the IT companies building it all across the United States.