Why single-part tests are not enough
Imagine two components that each pass all of their own tests. One produces a date as text, and the other reads a date as text. Both seem fine until it turns out that one writes day-month-year and the other expects month-day-year. Neither component is wrong in isolation. The mistake lives in the agreement between them.
This pattern is common. Unit tests check that a small piece behaves as its author intended. They cannot tell you whether the pieces were intended to fit together in the same way. That is what integration and system testing are for.
Integration testing
Integration testing checks that two or more components work correctly when combined. The scope is deliberately limited: usually one connection or one small group of parts, not the entire product.
Typical examples:
- Code that saves and reads records through a real database.
- A module that calls another module, with both real.
- Software that reads from or sends commands to a device or another service.
- A web service handling a request and returning the response the caller expects.
What integration tests tend to catch:
- Mismatched assumptions about formats, units, ordering or the meaning of fields.
- Wrong or missing configuration, such as an incorrect connection setting.
- Error handling across a boundary, for example what happens when the other side is slow, unavailable or returns something unexpected.
- Data that survives one step but is damaged in the next, such as characters lost when text passes through a layer.
Because many faults sit at boundaries, many engineers find that interfaces deserve extra attention in any design; the same idea appears in systems engineering when people look at system interfaces.
Approaches to combining parts
Teams combine components in different ways. Some join everything at once and test the result, which is simple but makes failures hard to locate. Others add one component at a time, testing at each step, so a new failure points to the newest addition. Incremental combination is usually easier to debug, although it takes more planning.
Test doubles
Sometimes a real dependency is impractical: it is slow, costs money, is not available yet or cannot be made to fail on demand. A test double, such as a stub or a fake, stands in for it. Doubles make tests faster and more controllable, but they carry a risk: the double only behaves as well as the author’s understanding of the real thing. If that understanding is wrong, the tests pass and the real combination still fails. Many projects keep at least a few tests against the real dependency for this reason.
System testing
System testing exercises the complete, assembled product in an environment that resembles real use, judging it from the outside, as a user or a connected system would. It asks whether the whole thing meets its requirements.
System tests typically cover:
- Complete user journeys from start to finish.
- Behaviour across all components, including start-up and shut-down.
- Non-functional properties such as response time, behaviour under load, recovery after failure and resource use.
- Compatibility with the platforms and configurations that the product must support.
Because they involve everything, they catch problems that smaller tests structurally cannot, such as an unexpected interaction between two features or a performance problem that appears only with realistic amounts of data. They are also the slowest, the most expensive to maintain and the hardest to diagnose. A system test failure says that something is wrong somewhere; finding where may take real investigation, using the techniques described under debugging strategies.
For products with hardware, the equivalent step often involves physical equipment, which brings its own planning needs.
What each level catches
| Level | Typical scope | Tends to catch | Typical cost |
|---|---|---|---|
| Unit | One function or class | Logic errors, boundary mistakes | Low, fast |
| Integration | A few components together | Interface mismatches, configuration, boundary errors | Medium |
| System | The whole product | Feature interactions, end-to-end flaws, performance | High, slow |
No level replaces the others. A failure in a system test that no unit or integration test predicted is a hint that a lower-level test is missing, and adding one often makes the problem easier to prevent next time.
Choosing the balance
A common recommendation is to have many unit tests, fewer integration tests and a small number of system tests, because lower-level tests are faster and more precise. That is a guideline rather than a rule. A product that is mostly a thin layer connecting external services may reasonably have little interesting logic to unit test and more value in integration checks.
Some practical points:
- Keep each integration test focused on one connection so failures are easy to explain.
- Make test environments repeatable. Tests that depend on a shared database in an unknown state tend to become unreliable.
- Reserve system tests for the journeys that matter most to users and the risks that matter most to the business.
- Run the quick tests on every change and the slower ones less often, using an automated pipeline such as the one described under continuous integration.
- Link tests back to what they verify; the habit of tracing requirements to tests helps show which requirements have system-level checks and which do not.
Limits to keep in mind
Combined-component tests are more realistic, but they are not complete. An environment that resembles production is still not production, and some failures only appear under real conditions or rare timing. Where software is involved in safety-critical or regulated work, the amount and type of testing is often set by standards that differ by country and industry, so follow those and the qualified people responsible for them rather than relying on a general guide.