Ch. 22 · Software Testing

Unit and Integration Test Balance

Separate fast isolated unit tests from integration tests that use real dependencies, and find the balance that catches real bugs.

~2 min readintermediateupdated Oct 5, 2026

A unit test isolates a small piece with its dependencies replaced, so it runs fast and pinpoints failures. An integration test exercises several real components together, so it catches the wiring and contract bugs that unit tests miss. Both are needed.

Before you start

You should be comfortable with mocks and test structure. This article covers the two levels and how to balance them.

Step-by-step walkthrough

Step 1: Unit tests for logic and edge cases

A unit test replaces dependencies with fakes or stubs and asserts the unit’s behavior, which is fast and deterministic. Use them to cover branches, boundary values and error handling in a function, where a precise failure message helps.

Step 2: Integration tests for wiring and contracts

An integration test uses real components: a real database, a real HTTP client pointed at a fake server, or several modules together. It catches serialization mismatches, SQL errors and misconfigured wiring that isolated tests cannot see.

Step 3: Balance the cost and confidence

Unit tests are cheap and many, integration tests are slower and fewer, so a suite leans on units with a focused set of integration tests around the risky boundaries. The exact shape depends on the system, but all-mocked suites and all-integration suites both have failure modes.

Worked scenario

The unit test isolates logic; the integration test proves the query.

test('discount applies above threshold', () => {
  expect(discount(120)).toBe(12);       // pure unit
});
test('order repository saves and reads back', async () => {
  await repo.save(order);
  expect((await repo.findById(order.id)).total).toBe(order.total); // real DB
});
JavaScript

Walk through the example

The first test checks the discount rule in isolation, fast and precise. The second uses a real database to prove the mapping and query work, which no mock would catch. Together they cover the rule and the wiring.

Common mistake

Mocking everything, so the suite passes while the real serialization or query is broken. The opposite is making every test an integration test, so the suite is slow and a failure does not say which layer broke.

Verify the behavior

Break a query or a serialization mapping and confirm an integration test fails while unit tests pass, proving the layer they cover. Change a business rule and confirm the unit test fails fast. Time both levels to keep the suite practical.

Interview exercise

Why can a fully mocked suite pass while the app is broken?

Answer and reasoning

Because mocks encode assumptions about dependencies rather than their real behavior. If the real service changes its response shape or the database rejects a query, the mock still returns the expected value, so the test stays green while production fails. Integration tests exercise the real contract and catch that.

Continue learning

Compare isolation in Mocks, stubs and fakes and contracts in Contract testing. Read the Martin Fowler on the test pyramid and try the Testing interview questions.

More in Software Testing

read ✓Software Testing · hard

Testing Time with an Injected Clock

Inject a clock instead of calling the system time, freeze it in tests, and remove the sleeps that make tests flaky.

~2 min readread →
read ✓Software Testing · mid

Code Coverage and Its Limits

Read line and branch coverage as a signal, not a goal, and avoid the tests that chase the number without checking behavior.

~2 min readread →
read ✓Software Testing · hard

Scoping End-to-End Tests

Reserve end-to-end tests for critical user journeys, use stable selectors, and avoid duplicating coverage the lower tests already have.

~2 min readread →
esc