Skip to content

What is integration testing?

Integration testing checks that separately built parts of a system work together, e.g. a service and its database, rather than testing each part alone.

Last updated , 9 min read

What is integration testing?

Integration testing is a level of software testing that checks whether two or more parts of a system work together once they are connected. A test usually joins real parts at a boundary, e.g. a service and its database. It catches failures that appear only where parts meet, even when each part passes its own tests.

The International Software Testing Qualifications Board (ISTQB) defines integration testing as "a test level that focuses on interactions between components or systems." A unit test sits one level below and checks one part alone, with its neighbors replaced.

System testing sits one level above and checks the complete system against its requirements. A test that crosses a real boundary, e.g. a call to a database, usually counts as an integration test.

How does integration testing work?

An automated integration test usually runs in these steps:

  1. The test runner starts the parts under test, e.g. two services and a test database.
  2. The test loads known data, e.g. a cart that holds 2 items.
  3. The test calls the first part, e.g. a function that saves an address.
  4. The first part sends a request across the boundary to the second part.
  5. The test reads the stored result and compares it with the expected result.
  6. The runner deletes the test data or discards the database.

Parts outside the boundary under test can still be replaced with a test double, e.g. a payment provider. A unit test replaces the second part too, so the request in step 4 never reaches it.

Integration tests catch failures that no part shows alone:

  • Mismatched data. One part sends a field that the other part reads under another name.
  • Different meanings. One part sends a change to one field, and the other part replaces the whole record.
  • Rejected queries. The real database rejects a query that a test double accepted.
  • Wrong wiring. A part calls the wrong address, or a table is missing because a migration did not run.

What is an example of an integration test?

Here is an illustrative example. Acme Co. sells furniture online, and its checkout service stores carts in a separate cart service. A developer asks a coding agent to "Let customers edit their delivery address during checkout." Three steps lead to the failure:

  1. The agent adds saveAddress to the checkout service. It sends PUT /carts/C-2210 with the body {"address": "12 Elm Street"}.
  2. The agent's unit test replaces the cart client with a mock that checks the address in the request. The test passes.
  3. The cart service treats PUT as a full replacement, as the HTTP standard, RFC 9110, defines it. It stores a cart with the address and no items.

A reviewer writes an integration test that starts both services and a test database:

test("saving the address keeps the cart items", async () => {
  const cart = await cartService.createCart({ itemCount: 2 });

  await checkout.saveAddress(cart.id, "12 Elm Street");

  const saved = await cartService.getCart(cart.id);
  expect(saved.address).toBe("12 Elm Street");
  expect(saved.items).toHaveLength(2);
});

The test fails on its last line:

expect(received).toHaveLength(expected)

Expected length: 2
Received length: 0
Received array:  []

Each service passed its own tests, and the failure sat in the agreement between them.

One request, sent to a mock and to the real service Unit test Checkout service PUT Mock cart client accepts the request Passes. Nothing stores the cart. Integration test Checkout service PUT Cart service replaces the whole cart Test database Fails. The cart has 0 items.
The checkout code is the same in both rows. Only the integration test sends the request to the real cart service, so only it shows what that service does with the request.

The agent changes saveAddress to send PATCH, which the cart service applies as a partial update, and the test passes.

This example is simplified. A real project would also test the other callers of the cart service, e.g. the page that adds items.

What are the types of integration testing?

Integration testing has four common types, which join the parts in different orders:

  • Big-bang integration. All parts join at once, and then the whole is tested. A failure can come from any interface, so its cause is slow to find.
  • Top-down integration. Testing starts with the parts at the top, e.g. the checkout page. Stubs return fixed answers in place of lower parts until the real ones are added.
  • Bottom-up integration. Testing starts with the lowest parts, e.g. the code that writes carts. A driver, a small program that calls the part under test, stands in for the parts above.
  • Sandwich integration. Also called hybrid or mixed integration, it adds parts from the top and the bottom and meets in the middle.

Incremental integration covers the last three types, because each adds parts a few at a time.

The ISTQB Foundation Level syllabus also names two levels. Component integration testing checks the interfaces between the components of one system. System integration testing checks the interfaces with other systems and outside services, preferably in an environment similar to production, e.g. a staging environment. In the syllabus's example, developers do the first and a test team does the second.

Martin Fowler describes two meanings of the term. Narrow integration tests exercise only the code that talks to another service, with test doubles for that service. Broad integration tests need live versions of all services, and Fowler prefers other names for them, e.g. end-to-end test.

What changes when a coding agent writes the code?

A coding agent usually works in one repository, while the service across a boundary often lives in another. Its tests then often replace that service with a mock that the agent generates from its code and prompt, not from the service's rules. In the Acme example, nothing in the agent's context described how the cart service handles PUT. The mock accepted the request, and the real service replaced the whole cart.

Two parallel agents can each change one side of an interface on separate branches. One renames a field in the cart service, while the other adds code that reads the old name. Each branch passes its own tests, and the mismatch appears only after both merge.

A team can give the agent's session one command that starts the real neighboring services and a test database. When the cart service lives in another repository, a contract test records the requests checkout sends and the answers it expects. The cart service's pipeline then checks those answers against the real service. The team can also rerun the integration tests on the merged code in its continuous integration and delivery (CI/CD) pipeline, to catch a mismatch between branches.

Should integration tests use a real database?

Integration tests of database code should usually run against a real database of the same engine and version as production, e.g. in a Docker container. A substitute from another engine can accept what production rejects. SQLite, by default, stores the text 'two' in a column declared as INTEGER, while PostgreSQL rejects the insert.

The usual objections to database tests each have a common answer:

  • Speed. Keep database tests to the code that talks to the database, and cover the rules above it with unit tests.
  • Shared state. Give each test its own records, e.g. with unique IDs that the test deletes afterwards, to keep test isolation.
  • Different setups. Build a fresh database from the project's migrations and known seed data on each run, e.g. in an ephemeral environment. Seeding it is part of test data management.

A database version that differs between a laptop and the CI runner is one reason integration tests fail in CI but pass locally.

How is integration testing different from end-to-end testing?

Integration testing checks one or more boundaries between parts, usually below the user interface. Its tests often run faster, and a failure leaves fewer parts to search. End-to-end testing runs the whole system through the interface a user touches, e.g. a web page. The table that compares integration and end-to-end tests with unit tests sets out where each level runs and what it misses.

How do you check a whole workflow after integration tests pass?

Integration tests check only the boundaries someone chose to test, usually below the interface a customer uses. Run the assembled software from its interface through the workflows a change touches, and check what it does.

RunStory considers your prompts and code changes to decide what to test. It runs your software in a separate environment, tries relevant workflows, and checks the results. It is in private alpha for CLIs and web apps.

Join the RunStory alpha →

FAQs

Who writes integration tests?

Integration tests between components are usually written by developers, and tests between whole systems are often written by a test team, as in an ISTQB syllabus example. When a coding agent writes the code, a reviewer can write the integration test from the request, not from the agent's code.

What is big-bang integration testing?

Big-bang integration testing joins all the parts of a system at once and then tests the whole. A failure can come from any interface, so its cause takes longer to find than in incremental integration.

Do integration tests need their own environment?

Integration tests need an environment where the connected parts can run, e.g. a test database built for each run. Tests between components can run in an agent's session or a CI/CD pipeline. Tests between whole systems work best in an environment similar to production.

Can integration tests use mocks?

Integration tests can use mocks for parts outside the boundary under test, e.g. a payment provider. A mock at the boundary itself checks only a guess about the other part. Fowler's narrow integration tests use such doubles, and contract tests can check them separately.