Software Dev Notes

Home › Testing & Debugging

Unit vs Integration Testing: What Each Catches and Where Each Fails

Compare unit and integration tests by scope, speed and failure diagnosis, and learn how to balance them in a test suite using the test pyramid.

2026-10-19 · 5 min read

Every team that writes tests eventually asks the same question. Should we test small pieces of code in isolation, or should we test the system as a whole? The honest answer is that both kinds of test answer different questions, and each fails in different ways. Unit tests are fast and pinpoint bugs, but they can pass while the parts do not fit together. Integration tests prove that the parts connect, but they are slower and harder to diagnose. This post explains what each kind of test covers, where each one breaks down, and how to combine them into a suite that gives reliable feedback.

What a unit test covers

A unit test checks one small piece of behavior, usually a function or a class, in isolation. Dependencies such as databases, network calls and the file system are replaced with fakes, stubs or mocks that return fixed answers. The test sets up an input, calls the unit, and asserts on the output or on the calls made to its collaborators.

The strength of this approach is focus. When a unit test fails, the failure points to a small region of code, because everything else was replaced by a controlled stand-in. Unit tests also run in milliseconds and need no servers, so developers can run thousands of them after every change. That speed changes behavior: people run tests more often, and bugs are caught while the code is still fresh.

What an integration test covers

An integration test checks that two or more real components work together. Typical examples include a service talking to a real database, an HTTP handler calling real business logic, or a parser reading a real file from disk. The goal is to exercise the seams, where data crosses from one component to another and where assumptions about formats, timing and error handling can go wrong.

Integration tests often catch problems that unit tests cannot see. A unit test with a fake database might accept a query that the real database rejects. A mock might return a field name that the real API has renamed. A test with a fake clock might miss a time zone bug that only appears when the real system formats a date. These are mismatches between the model and reality, and integration tests are the cheapest way to find them.

The cost trade-off

The main cost of integration tests is time and fragility. They start real services, touch disks and networks, and depend on configuration. Each of these dependencies can fail for reasons unrelated to the code under test. When an integration test fails, the diagnosis may require reading logs from several components, which is slower than reading one unit test's assertion.

A common measure of suite health is the number of tests that fail intermittently. Unit tests tend to be deterministic, because their inputs are fully controlled. Integration tests can be flaky because of timing, ports left open by an earlier run, or shared state in a test database. Good integration suites isolate each test with fresh data, avoid fixed sleeps, and wait on explicit conditions instead.

The test pyramid

The test pyramid is a widely used model for balancing these trade-offs. It was popularized by Mike Cohn in his writing on agile testing. The model suggests a broad base of fast unit tests, a smaller middle layer of integration tests, and a narrow top of end-to-end tests that drive the whole system through its user interface or public API. The exact shape is a guideline, not a law, but the principle holds: the cheaper and faster the test, the more of them you should have.

Test type Scope Typical speed Failure diagnosis Main risk
Unit One function or class Milliseconds Very precise Fakes drift from reality
Integration Several real components Seconds Needs log reading Flakiness and setup cost
End-to-end Whole system Minutes Hardest Slow and brittle

The table shows typical ranges. Your numbers will differ, but the ordering is usually the same, and the gap widens as systems grow.

Where unit tests mislead

A frequent failure is the test that passes because it tests the mock. If a unit test sets up a fake to return success and then asserts that the code returns success, the test has verified only the test. Good unit tests assert on behavior the code controls, such as calculations, branching decisions and error mapping, rather than on the internal calls made to a stand-in.

Another failure is over-mocking. When a test mocks every collaborator, it becomes coupled to the internal structure of the code. Renaming a private method breaks many tests even though behavior is unchanged. This makes refactoring painful and discourages improvement. Prefer simple fakes that behave like the real thing, such as an in-memory repository that implements the same interface, over mocks that record every call.

Where integration tests mislead

Integration tests can give false confidence when they cover only the happy path. A test that saves an order and reads it back proves the basic flow, but it may not check what happens when the database is unavailable, when a unique key is violated, or when two requests race for the same resource. Choose integration cases that represent the failures you expect in production.

Integration tests can also hide slow code. A test that takes two seconds may pass easily, while the same code under load would time out. Performance-sensitive paths need explicit measurement, not only correctness checks.

A practical balance

For most application code, a useful split is to write many unit tests for pure logic such as pricing rules, parsers and state machines. Write a moderate number of integration tests for each adapter, such as the database layer, the HTTP client and the file reader, using real dependencies where practical. Add a small set of end-to-end tests for the most important user journeys, such as signing up, placing an order or exporting a report.

Run unit tests on every save or commit. Run integration tests on every merge request or nightly build, depending on cost. Run end-to-end tests before a release or on a staging environment. Matching the cost of a test to the moment its feedback is needed keeps the team moving without sacrificing confidence.

Writing tests that stay useful

Name tests after the behavior they check, such as "rejects an expired coupon," rather than after the method name. Keep each test focused on one reason to fail. Use builders or factories to create test data, so changes to a model do not require editing dozens of tests. Remove or fix tests that are flaky, because a suite that people ignore provides no protection at all.

Key takeaways

Unit tests check small pieces of behavior in isolation. They are fast and point directly at the cause of a failure, but they can pass while the real components disagree. Integration tests check that real components cooperate, catching mismatches that fakes hide, at the price of speed and diagnosis effort. The test pyramid suggests many unit tests, a healthy layer of integration tests and a few end-to-end checks. Good suites avoid testing mocks, cover failure paths, and stay deterministic, so that a red build always means something.

More in Testing & Debugging

Testing

How Debuggers Work: Breakpoints, Single-Stepping and Debug Symbols

Learn how debuggers pause programs with breakpoints, single-step through instructions, and map machine state back to source lines using debug symbols.

5 min read