Integration Testing vs Unit Testing: A 2026 Practitioner Guide

Written By  Crosscheck Team

Content Team

October 9, 2025 14 minutes

Integration Testing vs Unit Testing: A 2026 Practitioner Guide

Unit Tests vs Integration Tests: What to Write, When, and Why

A unit test verifies one function in isolation, with everything around it mocked. An integration test verifies that two or more real components — your code plus a database, an HTTP client plus a service, a queue plus its consumer — behave correctly together. Both are essential, both catch different bugs, and the ratio between them is one of the most consequential decisions a team makes about how it ships software. This guide covers what each is, how the test pyramid has aged since Mike Cohn drew it in 2009, the Honeycomb and Trophy alternatives, and which tools — Jest, Vitest, Pytest, Supertest, Pact, Testcontainers — earn their place in a 2026 stack.

Key takeaways:

  • Unit tests target a single function with mocked dependencies — fast (milliseconds), cheap, best for branch coverage of complex logic.
  • Integration tests target two or more real components together. They are slower but catch the bugs that live in the seams.
  • The classic 70/20/10 test pyramid is a useful default but not a law. Spotify's Testing Honeycomb and Kent C. Dodds' Testing Trophy push the centre of gravity toward integration tests for microservices and frontend work.
  • 2026 default toolkit: Vitest or Jest for unit (Pytest in Python), Supertest for HTTP integration on Node, Testcontainers for real-database integration, Pact for contracts between services.
  • The wrong ratio costs more than the wrong test. Fit the shape to the architecture.

What Is a Unit Test?

A unit test verifies the behaviour of a single function, method, or class with every external dependency replaced by a mock, stub, or fake. The defining quality is isolation — the test exercises the logic under examination and nothing else. If the function calls a database, the database is mocked. If it calls an HTTP API, the HTTP client is faked.

Unit tests are written by the developer writing the code, usually in the same commit. Because they exercise only logic — no I/O, no network, no disk — they run in milliseconds, which means a typical Node.js or Python project can ship thousands of them in well under a minute. That speed is the whole point: unit tests are how engineers catch regressions in the seconds between saving a file and the next keystroke. The canonical structure is Arrange-Act-Assert — set up the inputs, invoke the function, assert on the output.

Here is a unit test in TypeScript using Vitest, exercising a pricing function:

// pricing.ts
export function applyDiscount(price: number, code: string): number {
  if (price < 0) throw new Error('price must be non-negative');
  if (code === 'SUMMER25') return Math.round(price * 0.75 * 100) / 100;
  if (code === 'VIP') return Math.round(price * 0.5 * 100) / 100;
  return price;
}

// pricing.test.ts
import { describe, it, expect } from 'vitest';
import { applyDiscount } from './pricing';

describe('applyDiscount', () => {
  it('returns the original price when no code matches', () => {
    expect(applyDiscount(100, 'INVALID')).toBe(100);
  });

  it('applies a 25% summer discount', () => {
    expect(applyDiscount(100, 'SUMMER25')).toBe(75);
  });

  it('throws on a negative price', () => {
    expect(() => applyDiscount(-1, 'VIP')).toThrow(/non-negative/);
  });
});

Notice what is absent — no database, no HTTP, no file I/O. The function takes inputs and returns outputs, and the test verifies the relationship between them. This is the ideal shape for a unit test, and it is why pure functions are the best candidates in any language.

Good at: logic errors in a specific function, instant feedback without infrastructure, a precise safety net for refactoring, documenting intended behaviour.

Blind to: whether two components actually work together, whether a database schema matches the queries run against it, whether an API contract is honoured, whether production configuration is correct.


What Is an Integration Test?

An integration test verifies that multiple components — your code plus at least one real collaborator — behave correctly together. Where the unit test mocks the database, the integration test starts an actual Postgres container and asserts that the SQL is correct. Where the unit test mocks the HTTP client, the integration test sends a real request through Supertest and asserts on the response. The seams that the unit test obscured are exactly the part the integration test exists to probe.

These tests sit between unit tests and end-to-end tests in cost. They take seconds rather than milliseconds, they need infrastructure (Docker, a test database, a queue), and they fail in more interesting ways. That price buys confidence in the things no unit test can verify — that the ORM generates valid SQL, that indexes exist, that the JSON payload one service emits matches what the consumer expects, that the connection string in staging.env points at a reachable host.

Here is an integration test in JavaScript using Jest and Supertest, exercising a real Express handler:

// app.js
const express = require('express');
const app = express();
app.use(express.json());

app.post('/orders', (req, res) => {
  const { items } = req.body;
  if (!Array.isArray(items) || items.length === 0) {
    return res.status(400).json({ error: 'items required' });
  }
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  return res.status(201).json({ orderId: 'ord_123', total });
});

module.exports = app;

// app.test.js
const request = require('supertest');
const app = require('./app');

describe('POST /orders', () => {
  it('rejects an empty items array with 400', async () => {
    const res = await request(app).post('/orders').send({ items: [] });
    expect(res.status).toBe(400);
    expect(res.body.error).toBe('items required');
  });

  it('creates an order and returns total', async () => {
    const res = await request(app)
      .post('/orders')
      .send({
        items: [
          { price: 10, qty: 2 },
          { price: 5, qty: 1 },
        ],
      });
    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({ orderId: expect.any(String), total: 25 });
  });
});

This exercises Express's routing, body parsing, validation, and JSON serialisation — none of which a unit test calling the handler directly would have caught.

Here is the Python equivalent against a real Postgres database spun up by Testcontainers:

# test_orders_integration.py
import pytest
from sqlalchemy import create_engine, text
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="module")
def db_engine():
    with PostgresContainer("postgres:17") as pg:
        engine = create_engine(pg.get_connection_url())
        with engine.begin() as conn:
            conn.execute(text("""
                CREATE TABLE orders (
                    id SERIAL PRIMARY KEY,
                    total NUMERIC NOT NULL CHECK (total >= 0)
                )
            """))
        yield engine

def test_orders_check_constraint_rejects_negative(db_engine):
    with db_engine.begin() as conn:
        with pytest.raises(Exception):
            conn.execute(text("INSERT INTO orders (total) VALUES (-1)"))

A unit test that mocked the database would happily accept the negative total and tell you nothing about the schema. The integration test catches it because the constraint lives in the database, not the application code — and the constraint is what production will enforce.

Good at: mismatches between components, SQL and indexes against a real database, configuration errors that mocks can never see.

Blind to: granular branch coverage of pure logic, the millisecond feedback loop that makes TDD viable, full end-to-end validation through a real browser.


Unit Test vs Integration Test: The Differences at a Glance

DimensionUnit TestIntegration Test
ScopeOne function or classTwo or more real components
DependenciesMocked, stubbed, fakedReal (or close: Testcontainers, WireMock)
SpeedMillisecondsSeconds to minutes
Setup costNoneDocker, test DB, fixtures
CatchesLogic errors, edge cases, refactor regressionsContract mismatches, SQL bugs, config drift
AuthorThe developer writing the featureDeveloper or QA engineer
ApproachWhite-boxCloser to black-box
Run frequencyEvery save, every commitEvery PR, every CI run
Failure signalPoints to a line of codePoints to a seam between components

The distinction matters because each layer catches the other's blind spots. A unit test confirms the discount calculation handles negative inputs correctly while being silent about whether the result is ever persisted to the right column. An integration test catches the persistence bug but might not exhaust every branch of the calculation. A test suite without both is missing half its job.


The Test Pyramid (and Its Critics)

The test pyramid is the most cited mental model in automated testing — many unit tests at the base, fewer integration tests in the middle, very few end-to-end tests at the top. Mike Cohn introduced it as the "Test Automation Pyramid" in his 2009 book Succeeding with Agile (he had been drawing the shape in talks since around 2003-2004, and Martin Fowler later expanded the model in his bliki). The three original layers — Unit, Service, UI — are what the industry now usually calls Unit, Integration, and E2E.

The default proportions practitioners cite are roughly 70% unit, 20% integration, 10% E2E, though Cohn himself never pinned specific numbers. The shape is the point. Unit tests at the bottom because they are cheap, fast, and numerous. Integration tests in the middle because they cost more and there should be fewer of them. End-to-end tests at the top because they are slow and brittle and earn their place only for the most critical flows. The shape Cohn warned against is the ice cream cone — many E2E tests, few integration tests, almost no unit tests. Ice cream cone suites are slow, flaky, and expensive to maintain. They provide worse signal than a well-distributed pyramid at a fraction of the infrastructure cost.

Testing Honeycomb (Spotify, 2018)

In January 2018, Spotify engineer André Schaffer published Testing of Microservices arguing that the pyramid had stopped fitting microservices. In a service-oriented architecture, the biggest complexity is not within a single service but in how services interact — and a suite weighted toward unit-level coverage misses exactly that.

Spotify's proposal was the Testing Honeycomb: a wider middle (integration tests), a narrower top (what they called "integrated tests" — tests that pass or fail based on another team's system, which they argued should be vanishingly few), and a smaller bottom of "Implementation Detail Tests." The point was not to abandon unit testing but to acknowledge that for a service whose job is mostly orchestration, exhaustively testing every internal helper is less valuable than verifying the service's interactions. The Honeycomb is Spotify's model, not Google's — a common attribution error. Google's web.dev team has published a useful survey of shapes ("Pyramid or Crab?") but did not originate any of them.

Testing Trophy (Kent C. Dodds, 2018)

Around the same time, Kent C. Dodds proposed the Testing Trophy for frontend work — Static Analysis at the bottom (TypeScript, ESLint), then a small layer of unit tests, then a large layer of integration tests, then a thin cap of E2E. The trophy emerged alongside React Testing Library, which Dodds released to encourage testing components the way a user interacts with them rather than asserting on implementation details.

The shared insight across Honeycomb and Trophy is that integration tests deliver the most confidence per unit of effort in two contexts: distributed services where the bugs live in the network, and component-heavy frontends where the bugs live in how components compose.

Practitioner Reality, 2026

The consensus in 2026 is closer to fit the shape to the architecture:

  • A pure-logic library (date parser, tax calculator, JSON schema validator) is best served by the classic pyramid — most bugs are in the logic.
  • A microservice with thin business logic and many external integrations earns the Honeycomb — most bugs live at the seams.
  • A React or Vue application earns the Trophy — TypeScript catches the cheap mistakes, integration tests with React Testing Library catch the rest, a small E2E suite covers the critical paths.

70/20/10 is a defensible default with no other information. The better question on any non-trivial codebase is which layer is currently catching the bugs that matter, and writing the next test there.


When to Lean on Unit Tests

Unit tests deliver the highest return when logic is complex, business rules are critical, or code is going to be refactored.

Pure business logic. Pricing calculations, eligibility rules, financial computations, validation — anything algorithmic. The behaviour is fully described by inputs and outputs, and there is nothing to mock.

Refactor surface area. A legacy module or a function with structure you know is wrong needs a unit-test cage around it before you touch the implementation. The tests pin down external behaviour so you can swap internals freely.

Algorithmic edge cases. Off-by-one errors, empty inputs, Unicode, leap-year arithmetic, timezone conversions. Branch coverage at the unit level is the only economical way to catch these.

TDD workflows. When the test is the specification, writing it at the unit level is the only practical choice — writing an integration test before any code exists rarely makes sense.


When to Lean on Integration Tests

Integration tests earn their slower runtime when the failure lives between components.

Database queries. Anything more complex than a single-table SELECT * deserves an integration test against a real database. ORMs lie, schemas drift, indexes silently disappear. A Testcontainers-backed test catches it on the next CI run.

External APIs and microservices. When your service calls another team's, the interesting failures are protocol-level — wrong content type, missing auth header, breaking change in a response field. An integration test, or better a contract test with Pact, catches these.

Configuration-driven behaviour. Most production incidents are not code bugs — they are configuration bugs. Integration tests run against real configuration and fail when configuration is wrong.

Third-party SDKs. Stripe, Auth0, SendGrid, Twilio. The wrapper code is easy to unit-test, but the actual SDK behaviour — how it serialises money values, what happens on a retry, how webhooks are signed — only surfaces against a real or sandboxed integration.


Tools That Earn Their Place in 2026

Unit Testing

Vitest (JavaScript/TypeScript). The default for new JS/TS projects in 2026. Vitest topped the State of JS 2024 testing satisfaction ranking, Angular 21 adopted it as the default test runner in late 2025 (deprecating Karma), and Nuxt and SvelteKit have recommended it for years. Vitest 4.0 shipped in October 2025. Native ESM, Vite-powered transforms, smart watch mode — typical reports put it 3-5× faster than Jest on the same suite.

Jest (JavaScript/TypeScript). Still the most-installed JS test runner. Meta maintains it and it pulls roughly 30 million weekly downloads on npm against Vitest's ~14 million in early 2026. If you are on Create React App, Next.js without Turbopack, or any pre-2024 codebase, Jest is already there and there is no urgency to migrate.

Pytest (Python). The default in Python. Clean assertion syntax, powerful fixtures, a rich plugin ecosystem (pytest-asyncio, pytest-mock, pytest-cov), and a monkeypatch fixture that is more pleasant for simple cases than unittest.mock. The standard library's unittest still ships with Python, but new projects almost universally pick pytest.

JUnit 5 + Mockito (Java), xUnit/NUnit (.NET), RSpec (Ruby). Mature, well-understood, no surprises.

Integration Testing

Supertest (Node.js). The standard for HTTP-level integration tests against Express, Fastify, Hono — anything exposing an http.Server. Around 8 million weekly downloads. Pairs cleanly with Jest or Vitest and invokes the request handler directly, no real network port required.

Testcontainers (Java, Python, Node.js, .NET, Go, Rust, Ruby). The cleanest way to run integration tests against real infrastructure. Spin up Postgres, MySQL, Redis, Kafka, LocalStack, or anything else from a Docker image, run the test, throw the container away. No shared test database, no contamination between runs.

Pact. The dominant tool for contract testing between services. Consumers declare what they expect from a provider, the provider verifies it can deliver, and the broker tells you whether the next deploy is safe. Pact supports JavaScript, Java, Python, Ruby, Go, and .NET. The newer bi-directional approach championed by PactFlow lets providers publish an OpenAPI spec instead of running consumer-driven tests directly.

WireMock and MSW. WireMock stubs external HTTP APIs with realistic, stateful mock servers. MSW (Mock Service Worker) does the same isomorphically across browser and Node — the 2026 standard for mocking the network in frontend test setups.


Common Pitfalls

Over-mocking unit tests. When a test mocks five or six dependencies to exercise one function, it ends up testing the test setup rather than the code. Either the function is doing too much and should be split, or the right test is an integration test.

Under-investing in integration tests. The classic failure mode — hundreds of unit tests, a handful of integration tests, and a steady drip of production incidents where two components disagree about a field name. If your postmortems keep landing on "the API returned a string but the consumer expected a number," your integration investment is too low.

Slow integration suites nobody runs. A 15-minute integration suite gets skipped. Use Testcontainers lifecycle hooks to share containers where possible, parallelise aggressively, and treat suite duration as a first-class metric.

Shared state between tests. Tests that leave data in a shared database, or depend on data left by a previous test, fail in order-dependent ways that take a day to debug. Each test owns its own setup and teardown.

Testing the mock, not the behaviour. A test that asserts expect(mockClient.send).toHaveBeenCalledWith(...) and nothing else asserts that the setup runs, not that the code works. The assertion should be on the outcome — the response, the database row, the message that ended up on the queue.


When Integration Bugs Surface During Manual QA

Automated tests cover a lot of ground, but they cannot replace human judgement during exploratory testing — and when something goes wrong in manual QA, the hardest part is rarely noticing the bug. It is capturing enough technical detail for a developer to reproduce it.

The tester sees the UI break. The actual cause is usually a 500 from a backend service, a malformed payload, a JavaScript error, or a race condition in how data loaded. Without the network trace, the console log, and the steps that led there, the bug report says "something broke" and the developer cannot move.

This is the gap Crosscheck closes. The free Chrome extension captures the full context of a session — screenshots, screen recording, console logs, network requests and responses, and the steps a tester took — and attaches all of it to a bug report sent directly to Jira, Linear, ClickUp, Slack, or GitHub. The developer gets the failed integration laid out: which request returned a 500, what payload was sent, what the console said, and exactly what the tester clicked to provoke it.


FAQ

Should I write unit tests or integration tests first?

If the logic is non-trivial — pricing, parsing, validation — start at the unit level. For glue code that mostly orchestrates other services, start at the integration level, because the unit test of orchestration is mostly a test of your mocks. The choice depends on where the risk lives.

What is the right ratio of unit to integration tests?

The classic pyramid says roughly 70% unit, 20% integration, 10% E2E. Spotify's Honeycomb pushes the centre toward integration for microservices; Kent C. Dodds' Trophy does the same for frontends. Pragmatically: measure where your bugs are escaping and write the next test at that layer.

Are integration tests the same as end-to-end tests?

No. An integration test exercises a few real components together — your code plus a database or another service. An end-to-end test exercises the full system, often through a real browser, against a near-production environment. Both have a place; they are not interchangeable.

Is the test pyramid still relevant in 2026?

Yes, as a default mental model. It is not the only shape, and for some architectures the Honeycomb or Trophy fits better. But the pyramid's core insight — fast feedback at the bottom is more economical than slow feedback at the top — has not been refuted.

Do I need contract tests if I have integration tests?

If you own all the services, integration tests are usually enough. If you depend on services owned by other teams or other companies, contract tests with Pact are the only practical way to catch breaking changes without coordinating every deploy.

What about Playwright or Cypress for end-to-end tests?

E2E tests sit above integration tests and exercise the real system through a browser. Valuable for critical flows (login, checkout, signup), dangerous if overused. See the Selenium vs Playwright vs Cypress 2026 comparison for a deeper take.


Closing the Loop Between Tests and Bug Reports

Unit tests and integration tests are complementary, not competing. Unit tests give fast, precise coverage of individual logic. Integration tests give confidence the pieces still fit. Both miss things — which is why manual QA still matters, and why the bugs that escape both layers need to land in front of the right engineer with the right context.

That last mile — turning "something broke" into "here is the failing request, the console error, the steps to reproduce, and a recording" — is what Crosscheck handles. Install the free Chrome extension, click to report, and the full technical context lands in Jira, Linear, ClickUp, Slack, or GitHub alongside the rest of your team's work. No paid tiers, no usage limits.

Try Crosscheck free

For wider reading on the testing stack: the best test automation frameworks for 2026, the best AI testing tools of 2026, and the perfect bug report template.

Related Articles

Contact us
to find out how this model can streamline your business!
Crosscheck Logo
Crosscheck Logo
Crosscheck Logo

Speed up bug reporting by 50% and
make it twice as effortless.

Overall rating: 5/5