Skip to main content

Search...

Integration Testing: Checking How Components Work Together

Components often pass their tests in isolation. It is in their interactions that the truly critical defects emerge.

What is Integration Testing?

The ISTQB defines integration testing as “a level of testing focusing on the interaction between components or systems” and further distinguishes between component integration testing and system integration testing. In practice the term is even broader: integration involves modules and classes just as much as services, layers, subsystems or entire systems from different vendors.

In distributed and service-oriented systems, interfaces are the most common source of defects. Components that function flawlessly in isolation fail precisely at their boundaries, where assumptions meet that were never verified together. Integration testing makes these weaknesses visible before they grow into hard-to-locate failures at the system level.

Positioning within the Test Levels

In the classical test level structure, integration testing sits between component and system testing. It requires at least two parts of the system to exist and to be connectable, and it picks up exactly where component testing ends: the focus is no longer on the individual unit but on its interaction with the other parts.

This boundary is not always sharp in practice, which is why distinguishing between two variants helps. Component integration testing verifies the interaction of modules, classes or layers within the same system. System integration testing verifies how multiple independent systems collaborate across interfaces and data exchange. The two differ considerably in test environment, test data and responsibilities. Ignoring those differences produces friction in the project.

Objectives and Quality Characteristics

The objective of integration testing is to demonstrate that the parts interact correctly across interfaces and workflows. The core questions are easy to ask and often hard to answer: is the exchanged data correct? Are its structures right? Are errors handled and propagated correctly?

The non-functional aspects belong to integration testing as well. How fast is the data exchange, how much load does the coupling tolerate, how robust does the connection stay when communication fails or times out? These questions are frequently skipped at the integration level. Later they resurface as hard-to-diagnose problems at the system level, and then their cause has to be traced back the tedious way.

Integration Strategies

The choice of integration strategy directly determines how quickly defects are found and how laborious their localisation becomes.

The big-bang approach integrates all components at once and then tests the assembled whole. It is simple to plan but hard to debug: when a test fails, isolating the cause in a fully interconnected system is a painstaking task.

Incremental strategies are therefore generally preferable. In the top-down approach, integration starts at the topmost layer and works its way down; components that are not yet available are replaced by stubs. In the bottom-up approach, one starts with the lowest-level components and builds upward; missing calling components are simulated by drivers. Both variants make fault localisation considerably easier, because the cause of a new failure is always to be sought first in the most recently integrated component.

In modern microservice architectures in particular, a clear integration strategy is often indispensable. The services communicate across network boundaries, and even the failure or delay of a single service can change the behaviour of the whole system.

Test Basis

The test basis for integration testing consists of all information that describes the interaction of the individual parts: interface specifications, sequence diagrams, use cases and API documentation, supplemented by the relevant architecture designs and design patterns.

The non-functional requirements on the interfaces belong to the test basis as well. Response times, throughput and fault tolerance on connection drops have to be specified explicitly and tested just as explicitly. Open or vague interface specifications are a frequent risk factor here: on their basis, errors get passed silently from component to component, and the eventual troubleshooting turns out correspondingly expensive.

Test Case Derivation

The proven structured design techniques work well for deriving test cases: equivalence partitioning and boundary value analysis on the basis of the interface specifications, decision tables for complex combinations of call parameters, state-based testing for stateful protocols and sessions.

Particular attention is due to the negative tests: test cases with invalid data formats, missing mandatory fields, incorrectly encoded character sets, empty responses or connection failures. These are exactly the cases that regularly bring interfaces and workflows down. Robust error handling at the boundaries does not emerge on its own, it has to be tested explicitly.

When creating test cases, gaps in the interface specification will inevitably come to light. These need to be resolved promptly with architects, developers or the business stakeholders, before tests are built on incorrect assumptions and then go on to verify the wrong thing.

Test Doubles

In incremental integration strategies, not all components are available yet, and it is precisely these gaps that test doubles fill. Stubs simulate called components with defined responses and thereby allow testing from the caller side without the real component existing yet. Drivers, conversely, simulate the calling components and initiate the flow under test. Mocks go one step further: they additionally verify that the interactions occurred correctly, that is whether the right method was called with the right parameters.

Test doubles enable independent testing without having to wait for the full development of all participants. They do want to be maintained, however. A stub whose behaviour diverges from the real component falsifies the test results and lulls the team into false confidence. Contract testing is a method designed to prevent exactly that.

Test Environment

The test environment for integration testing is more laborious to set up than for component testing. For component integration tests, the development environment is often still sufficient. For system integration tests, by contrast, a dedicated environment becomes necessary, one that includes all participating systems.

An important aspect here is monitoring. Anyone who wants to analyse the communication between the parts needs tools that make the data flow visible: logging, distributed tracing, API monitoring. It has to be kept in mind, though, that monitoring tools can themselves affect runtime. That becomes relevant when measuring interface performance.

Test Data

Test data management follows the respective integration level. For component integration tests, the test data can be defined locally, similar to unit tests. For system integration tests, the same requirements apply as for system testing: realistic, complete data constellations that reflect the actual data exchange between the systems.

What matters is that the test data covers the interfaces in all their variants: valid data, invalid data, boundary values, special cases. And it has to contain, deliberately, the data that provokes errors. Only this puts the error handling to a genuine test at all.

API Testing as a Core Tool

REST and GraphQL APIs are today the most common form of interfaces, and tools such as Postman, REST-assured or Karate enable automated testing directly at this level. They verify data formats, HTTP status codes, error behaviour and response times right at the interface, without taking the detour through a user interface.

API tests are less maintenance-intensive than GUI tests, provide fast feedback and integrate cleanly into CI/CD pipelines. For service-oriented and microservice architectures they are therefore the primary approach to integration testing.

Contract testing extends this approach by one decisive idea. Tools like Pact define contracts between the consumer and the provider of an interface, and these contracts can be verified independently of the other party, without both systems having to be really integrated at all times. This is particularly valuable when several teams develop independently and still have to ensure that their interfaces remain compatible.

Test Automation in Integration Testing

For development-close integration tests, automation runs almost by itself. For component integration tests there is an abundance of frameworks that embed directly into the build process, from JUnit through TestNG and pytest to Jest. The tests run on every commit and give immediate feedback.

It becomes more laborious at the higher integration levels. When two independent systems are to be tested together in an automated way and they differ technologically, the implementation effort rises considerably. A clear prioritisation pays off here: which interfaces are critical, which failure classes have the greatest impact? These are precisely the first candidates for automation.

Integration Testing in Agile Projects

Integration testing is especially important in agile projects and is nevertheless often neglected. The test pyramid allocates it its own layer, between the unit tests at the base and the system or UI tests at the top. Fast, stable API-level integration tests are the most effective way to ensure robustness both in the small and in the large.

In the microservice architectures that commonly emerge from agile teams, the question of integration testing arises with particular urgency. Because the services communicate across network boundaries, every change to one service can break dependencies in other services. Automated integration and contract tests are the most important safety line here, alongside the deployment process itself.

Typical Defect Classes and Risks

Certain defects typically become visible only in integration testing:

  • Incorrect or incompatible data formats at interfaces, for example different date formats, missing mandatory fields or wrong character encoding.
  • Missing or incorrect error handling for communication failures, timeouts or unexpected HTTP status codes.
  • Race conditions in asynchronous communication that only occur under load or at specific timing conditions.
  • Data consistency issues between subsystems after partial failures or failed transactions.
  • Security gaps at interfaces, such as missing authentication, missing authorisation checks or the unencrypted transmission of sensitive data.

The biggest risk, however, is an organisational one: that there are no integration tests at all. In many projects unit and system tests exist while the integration level is simply missing. And where integration does get tested, it is frequently unclear who bears responsibility: this system, that system or the team that defined the interface. Without clear ownership, experience shows that exactly one thing happens: nothing.

Limits of the Approach

Integration tests verify that the parts interact correctly. They do not establish the functional correctness of the complete system against the requirements, that is the job of system testing. Nor do they replace the solid component tests beneath them or the system test above them. They complement both without making either of them redundant.

In highly distributed systems with many asynchronous communication paths, classical integration tests also reach their limits. Here chaos engineering, the deliberate injection of failures into running systems, complements the classical approach with a perspective that can hardly be fully specified in advance.

From Practice

Integration testing is frequently postponed too long or skipped entirely in practice. The cause is usually a mixture of unclear responsibilities and the palpable effort required for the test environment. When a problem at an interface then surfaces, a laborious investigation begins that stretches across system and team boundaries. In the end it turns out considerably more expensive than the integration test one had saved oneself beforehand.

Frequently Asked Questions

In the big-bang approach all components are assembled at once and then tested together. In incremental integration, components are integrated step by step, which makes fault localisation much easier.

Wrong data formats at interfaces, missing or incorrect error handling for communication failures, race conditions in asynchronous communication and data consistency issues between subsystems.

API testing validates the interfaces between components directly, without involving the user interface. It is an efficient form of integration testing for service-oriented architectures.

Component integration testing verifies the interaction of modules, classes or layers within the same system. System integration testing verifies how multiple independent systems work together across interfaces and data exchange. The two differ significantly in test environment, test data and responsibilities.

Test doubles (stubs, mocks, drivers) replace components that are not yet available during incremental integration testing. Stubs simulate called components with defined responses. Drivers simulate calling components. They allow tests to run stably and independently of the development sequence.

Software Quality is Attitude, Not Methodology

Richard Seidl coaches teams and leaders towards a genuine quality culture.