What Is Integration Testing? An API Example

What is integration testing? It checks that connected parts of an app work together. See the definition, the kinds, and a worked API example.

Mustafa BayramogluMustafa BayramogluAbstract illustration of two separate paths meeting at one junction

Integration testing is a testing level that joins separate parts of an application and runs them together, to see whether they cooperate. For an API, that means sending real requests to the running app and checking that the routes, the auth, the database and the data passed between them agree.

What integration testing is

DataCamp defines integration testing as the process of combining two or more software modules and testing them as a group. It verifies that components or services work correctly together as a combined system. Supatest lists what it checks: data contracts, API responses and database connections.

A unit test checks one piece alone, with its neighbors faked. An integration test asks what happens once the neighbors are real. If you want the full comparison, see unit tests vs integration tests. For how an integration test differs from a smoke test, see smoke test vs integration test.

An API integration test, step by step

Take a shop API where a signed-in user can create an order and read it back. Two routes have to agree: the one that writes the order and the one that reads it. A unit test can check each handler alone. An integration test sends both requests to the running app.

Here is one in plain JavaScript, with the built-in fetch and Node’s test runner:

import { test } from 'node:test';
import assert from 'node:assert/strict';

const headers = {
  'Content-Type': 'application/json',
  Authorization: `Bearer ${process.env.TEST_TOKEN}`,
};

test('an order can be created and read back', async () => {
  const created = await fetch(`${process.env.APP_URL}/api/orders`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ productId: 42, quantity: 1 }),
  });
  assert.equal(created.status, 201);
  const order = await created.json();

  const read = await fetch(`${process.env.APP_URL}/api/orders/${order.id}`, { headers });
  assert.equal(read.status, 200);
  const body = await read.json();
  assert.equal(body.productId, 42);
});

Run it with APP_URL=http://localhost:3000 node --test against a copy of the app that is already running.

This one test passes through the auth check, the route, the JSON parsing, the database write, the database read and the JSON the app sends back. If any link is wrong, it fails. The second request is the one that matters. A 201 shows the handler answered. Reading the order back shows the data went into the database and came out again.

The test is also a small version of a flow: a sequence of requests where one response feeds the next. For the wider picture, see our guide to what API testing is. For passing a token or an ID from one request to the next, see testing multi-step API flows with auth tokens.

Kinds of integration test

There are three common kinds.

  • Database tests. Your data layer runs real queries against a real, often throwaway, database.
  • Service tests. Your code makes a network call to a different service, or to a fake standing in for it.
  • API-level tests. You run the app and call it over HTTP, as a browser or phone client would. Each request travels through the real routes, middleware, auth and database.

The order example above is an API-level test.

The word “integration” is used two ways. A narrow integration test covers just the code that talks to one neighbor, usually with a test double on the other end. A broad one needs live versions of all the services it touches, as Martin Fowler’s note on integration tests puts it. Integration is one of five common types of API testing.

What integration tests catch that unit tests miss

Unit tests have blind spots that integration tests cover. Wiring is one: middleware order, which router is mounted where, which handler a route points to. Serialization is another: the JSON your API sends out and the JSON your client reads back in.

In our unit test experiment, a commit renamed the login token and updated the unit tests that read it. All 2,189 tests passed, yet the web app kept reading the old name and nobody could log in. An API test that logs in and uses the token on the next request fails at the step a user would fail on.

Integration tests are not a replacement. Across 8 bugs, the unit tests caught 5 and one API flow test caught 6. Each side found bugs the other did not, and one bug slipped past both. Rerunning either kind after every change is regression testing, which regression testing vs unit testing compares with unit testing.

The same bug as an API test

In an API test, the login rename shows up as a 401 on the request right after the login, because that request still sends the old token field. The test fails at the step a user would fail on. The unit-vs-integration guide walks through that flow step by step in one bug, two tests.

How to write an API integration test

The shape is the same each time.

  1. Start the app with a test configuration and a throwaway database, so a failed run leaves nothing behind.
  2. Seed only what the test needs, such as one user and one product.
  3. Send the requests in the order a client would, and feed what one response returns into the next request.
  4. Assert on the status and on the fields in the body. A 200 with the wrong data inside is still a bug.
  5. Reset the database before the next run.

Keep each test to one journey. A test that logs in, creates an order and reads it back is easy to read when it fails. A test that does ten things is not.

When to write one

Write an integration test when:

  • data moves across a boundary, such as a response a client parses, a stored database row, or a message sent to another service;
  • a bug could come from how parts are wired together, like a route path, the order of middleware, or a config value;
  • the result depends on a sequence of calls, for example log in, then create, then update.

Keep unit tests for the logic inside each part: branches, edge cases and math. When you cannot tell which to write, ask what a user would see if the code broke. If the answer is an error on login, the bug sits between parts.

Integration tests vs end-to-end tests and the pyramid

A common way to list the levels is unit, integration, system and end-to-end testing. An integration test checks that a few parts work together. An end-to-end test follows a full user journey, typically through the real interface, and integration testing vs end-to-end testing sets the two side by side.

The usual advice is to write many unit tests, fewer integration tests, and very few end-to-end tests. An API-level integration test fits the middle layer: it uses the real app but skips the browser. The testing pyramid with an API layer shows where it sits, and the end-to-end testing guide covers the top of the pyramid.

Adding API integration tests to a project

You can write them by hand, as in the example, or record them. Stresseur takes the second route. It records how an API is really used during development, turns those flows into YAML tests, and runs them in CI on every pull request. Flows live in your repo under api-tests/. Generated flows check that a created item can be read back, as the example above does.

To try it on a built-in sample shop, run npx stresseur init --demo. See API integration tests from real traffic for how it works, then run them on every pull request.

Integration testing is the check that your parts work as a group. Start with the one journey your users cannot live without, test it through the API, and keep your unit tests for the logic inside each part.

Frequently asked questions

Why is integration testing done?

To catch bugs that live in the connections between parts, where each part passes its own tests. Middleware order and route mounting are wiring bugs, and a response the client reads differently than the server writes it is a serialization bug.

What's the difference between integration and system testing?

Integration testing checks interactions between modules inside one application. In the usual list of unit, integration, system and end-to-end testing, system testing comes after integration.

Who is responsible for integration testing?

Developers usually write integration tests, sometimes with QA. The person who changes a route or a response shape is best placed to add or update the test that covers it.

Do you write integration tests before or after unit tests?

After. Integration testing is commonly described as the second step in quality assurance, conducted after the unit tests.

How do I run integration tests on every pull request?

Put them in CI. Stresseur turns recorded flows into YAML tests and runs them in CI on every pull request. The flows live in your repo under `api-tests/`.

All posts