100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Software Testing

Types of Tests: Unit, Integration, and End-to-End

A practical breakdown of unit, integration, and end-to-end tests -- what each one verifies, when to use it, and how to tell them apart.

FoundationsBeginner9 min readJul 10, 2026
Analogies

Types of Tests: Unit, Integration, E2E

Not all automated tests check the same thing, and conflating them leads to suites that are either too slow or too shallow. Unit tests verify a single unit of logic in isolation, integration tests verify that multiple units cooperate correctly through their real interfaces, and end-to-end tests verify that an entire user-facing workflow behaves correctly from start to finish. Understanding what each type actually proves -- and what it doesn't -- is the key to deciding where to invest testing effort for a given feature.

🏏

Cricket analogy: Checking a single batsman's technique in the nets, a full XI's chemistry in a practice match, and the team's performance in an actual tournament are three genuinely different kinds of evaluation, just like unit, integration, and end-to-end tests check different scopes.

Unit Tests: Verifying a Single Piece in Isolation

A unit test targets the smallest testable piece of code, usually a single function or method, and isolates it from everything around it by replacing real collaborators -- a database connection, a network call, a clock -- with mocks, stubs, or fakes that return controlled, predictable values. This isolation is what makes unit tests fast: a well-written unit test runs in milliseconds because it never touches a disk, a network, or another process, and it makes failures easy to diagnose because there's only one piece of logic that could be responsible when it fails.

🏏

Cricket analogy: A bowling coach isolates a bowler's wrist position using slow-motion video against a static backdrop, removing the batsman, the field, and the crowd noise entirely so only the technique itself is being judged.

Integration Tests: Checking How Pieces Work Together

Integration tests check that two or more real components cooperate correctly across an actual boundary -- a service talking to a real (often containerized) test database, two microservices communicating over HTTP, or a payment module calling a sandboxed third-party API. Unlike unit tests, integration tests deliberately do not mock the thing being verified, because the whole point is to catch problems that only appear at the seam: a SQL query that's syntactically valid but returns the wrong rows against a real schema, a serialization mismatch between two services, or an API contract that changed without either side noticing.

🏏

Cricket analogy: A net session where the bowling machine is replaced with an actual fast bowler tests something a machine can't: how the batsman reads real seam movement and pace variation, the way an integration test checks a real database instead of a mock.

End-to-End Tests: Simulating the Real User

End-to-end tests drive the application the way a real user would, typically by automating a real or headless browser with a tool like Playwright or Cypress: clicking buttons, filling in forms, and asserting on what actually renders on screen. Because they exercise the full stack -- frontend, backend, database, and any third-party integrations -- end-to-end tests catch a class of bugs that lower-level tests structurally cannot, such as a CSS change that hides a submit button or a race condition that only appears when real network latency is present. The tradeoff is that they are slow, often taking seconds to minutes per test, and more prone to flakiness from timing issues, which is exactly why the pyramid keeps their numbers small.

🏏

Cricket analogy: A full day-night day of an actual Test match, with real weather, a real crowd, and a real opposing captain's field placements, reveals pressure-related mistakes that no amount of isolated net practice ever surfaces.

Choosing the Right Mix for Your Feature

Deciding how to allocate testing effort for a given feature comes down to asking what could go wrong and at what level that failure would actually be visible. A pure calculation, like computing a shopping cart's tax total, belongs almost entirely in fast unit tests because there's no meaningful interaction with other systems to verify. A feature that depends on a real database query benefits from an integration test that exercises the actual schema. A critical business flow, like checkout, deserves a handful of end-to-end tests covering the happy path and one or two important failure paths, but not dozens of E2E variations that duplicate what unit tests already cover more cheaply.

🏏

Cricket analogy: A selector doesn't send every uncapped player straight into a Test match to see if they're good enough; they use domestic scores, net sessions, and A-team tours to decide who's actually worth that expensive, high-stakes opportunity.

javascript
// paymentService.test.js
import { chargeCustomer } from './paymentService';
import { stripeClient } from './stripeClient';

jest.mock('./stripeClient');

test('chargeCustomer returns success when Stripe confirms the charge', async () => {
  stripeClient.createCharge.mockResolvedValue({ status: 'succeeded', id: 'ch_123' });

  const result = await chargeCustomer({ amount: 4999, customerId: 'cus_456' });

  expect(result.success).toBe(true);
  expect(stripeClient.createCharge).toHaveBeenCalledWith({
    amount: 4999,
    customer: 'cus_456',
  });
});

A quick rule of thumb: if a test's setup requires more mocking code than the assertion itself, it's usually a sign the test belongs one layer up, as an integration test against real infrastructure, rather than a unit test straining to fake an entire dependency graph.

  • Unit tests check a single function or class in isolation, using mocks or stubs to remove real dependencies.
  • Integration tests deliberately use real components across a boundary, such as a real test database, to catch seam bugs.
  • End-to-end tests drive the full stack through a real or simulated UI with tools like Playwright or Cypress.
  • Each layer catches a different class of bug: logic errors, contract mismatches, or full-workflow failures.
  • Choosing the right test type depends on where a given feature's risk actually lives, not on habit.
  • Overusing end-to-end tests for logic that unit tests already cover wastes CI time and adds flakiness.

Practice what you learned

Was this page helpful?

Topics covered

#SoftwareTesting#TestingTDDStudyNotes#SoftwareEngineering#TypesOfTestsUnitIntegrationAndEndToEnd#Types#Tests#Unit#Integration#Testing#StudyNotes#SkillVeris