Best practices

What Is Integration Testing? Types, Examples and How to Run It

Key takeaways

  • Test the connections between your app's parts, because pieces that pass their own tests can still fail together.

    A renamed field, a third-party API that changed or a missing environment variable only shows up when real components meet.

  • Start with the integration points where a failure costs money, access or data.

    List every place your code crosses a boundary, from service-to-service APIs to payments, email and sign-in, and rank them by what breaks if they fail.

  • Run the dependencies you own for real, and mock only the ones you can't run reliably in a test.

    A mocked third-party API keeps passing after the real one changes, so back each mock with a contract test or a run against the provider's sandbox.

  • Give every test its own data, in an environment nothing else uses.

    A test that depends on another test's leftovers, or on data someone changed by hand, fails at random.

  • Run integration tests on every pull request, and let a failure block the merge.

    Run the slower cross-service tests against staging after each deploy.

  • Cover every user flow that crosses several integrations with end-to-end tests too, critical ones first.

    Some failures only show up in a full workflow, through the real interface.

A team's unit tests can all pass while checkout fails, because the order service sends a field the payment service no longer accepts. Integration testing checks that separately built parts of an application (modules, services, databases and third-party APIs) work correctly together, by exercising the connections between them. It catches the bugs that live where one component hands data to another.

Integration testing sits between unit testing, which checks one function with its dependencies faked, and end-to-end testing, which runs a full user workflow through the interface. An integration test takes two or more real components and checks that they agree, for example that placing an order through the orders API reserves stock in the inventory service.

Testing an app with a lot of integrations? Colabra connects to dozens of third-party systems and ships new integrations during customer pilots, with QA Wolf maintaining its end-to-end tests. Book a demo to see how QA Wolf would test yours.

What is integration testing?

Integration testing is the level of software testing that checks how components behave once they're combined. A component can be a module in the same codebase, a separate service, a database, a message queue or an outside API such as a payment provider or an email service. The test sends real traffic across at least one of those connections and checks the result: the data arrived in the right shape, the right records were written, and errors were handled.

The term covers a wide range, which is why teams argue about what counts. Martin Fowler splits it in two. Narrow integration tests exercise only the code that talks to another service, often against a test double of that service. Broad integration tests run live versions of the services together. Both test a connection, which a unit test leaves out.

What's an example of integration testing?

Each of these passes only if both sides of a connection work and agree:

  • Orders and inventory: placing an order through the orders API reserves the stock in the inventory service, and an order larger than the stock is rejected without reserving anything.
  • Sign-up: the sign-up endpoint writes the user to the database, and the email service sends a confirmation with a working link.
  • Payments: checkout charges the payment provider's test mode and marks the order paid, and a declined card leaves the order unpaid with an error the user can read.
  • Webhooks: when the payment provider sends a "subscription renewed" event, the app extends the customer's plan.

Why does integration testing matter?

Integration testing matters because many bugs live in the gaps between components, where unit tests can't see them. A unit test replaces each dependency with a stand-in that behaves the way the developer expects, which keeps it fast. The real dependency may:

  • Expect a different format: a renamed field, a date in another time zone, a string where the code sends a number.
  • Change without warning: third-party APIs ship new versions on their own schedule.
  • Fail in ways the stand-in never does: timeouts, rate limits, expired tokens, and retries that charge a customer twice.
  • Be configured differently: wrong credentials, a missing environment variable, or a feature flag set one way in staging and another in production.

Integration tests catch these before customers do, close to the change that caused them, while the code is still fresh in the developer's mind.

Keeping up is the hard part, since every new integration adds paths to test, and every test has to run on every change and get fixed when the product moves. Integration tests usually stay with the developers who own the code. QA Wolf covers the same integrations from the user's side: with Coverage-as-a-Service, its AI agents write, run and maintain end-to-end tests that pass through those integrations, investigate every failure, fix broken tests and file real bugs. When an agent needs a question answered, it asks your team.

What are the types of integration testing?

The four classic approaches are big bang, top-down, bottom-up and sandwich (also called hybrid). Big bang tests everything at once; the other three are incremental, adding components a few at a time. Teams that run many services also use contract testing, and larger organizations run system integration testing across whole systems.

Big bang integration testing

Big bang integration testing combines all the modules at once and tests them together. It needs little planning and works for small systems. In a large one, a failure could come from any connection, so it's slow to trace, and testing can't start until every module is ready.

Incremental integration testing: top-down, bottom-up and sandwich

Incremental integration testing adds modules one or a few at a time and tests each new connection as it's added, so a failure points to the piece that was just added.

  • Top-down: start with the top-level modules, such as the UI or API layer, and replace lower modules that aren't ready with stubs (simple stand-ins that return fixed answers). The main flows get tested early, and the lowest layers last.
  • Bottom-up: start with the lowest-level modules, such as data access and utilities, and call them with drivers (small test programs that stand in for the callers). The foundations get tested first, and the user-facing flow comes together last.
  • Sandwich (hybrid): run top-down and bottom-up at the same time and meet in the middle layer. It suits large systems built by several teams, at the cost of more stubs and drivers to keep up.

Contract testing

Contract testing checks that two services agree on the requests and responses between them, without running both at once. The consumer records what it sends and what it expects back; the provider checks those expectations against its own code in its own build. If the provider changes a field the consumer relies on, the provider's build fails before the change ships. Pact is a common open-source tool for it. Contract tests catch breaking API changes early and cheaply, and they still need a few integration tests against the real services behind them, since a contract only covers what both sides wrote down.

System integration testing (SIT)

System integration testing checks a complete system together with the other systems it exchanges data with, such as a CRM, an ERP, a payment gateway or another team's platform. It usually runs in a staging environment set up like production, after the components have been integration tested and before user acceptance testing.

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

Unit tests check one piece of code in isolation, integration tests check that two or more pieces work together, and end-to-end tests run a whole user workflow through the real interface. Most teams need all three.

  • Unit testing: one function, class or module, with its dependencies replaced by test doubles. Runs in milliseconds, is usually written by the developer alongside the code, and runs on every commit. Catches logic errors.
  • Integration testing: two or more real components and the connection between them, such as a service and its database, two services, or your code and a third-party API. Takes seconds, needs an environment with those dependencies, and usually runs on every pull request. Catches mismatched data, broken contracts and configuration errors.
  • End-to-end testing: a complete user workflow through the UI or public API, with the real backend and third-party services behind it. Takes longer and needs a full test environment. Catches what users would hit, such as a flow that breaks because of how several integrations combine.

All three are kinds of functional testing: they check that the software does what it should, at different scopes.

The classic test pyramid, from Mike Cohn's Succeeding with Agile, says to write many unit tests, fewer integration tests and the fewest end-to-end tests. QA Wolf argues the test pyramid is a relic of a time when end-to-end tests were too slow and expensive to run widely. Some failures only show up in a full workflow. In that post's examples, a unit test won't catch a payment that looks successful but never completes on the backend, and an integration test won't catch a login that breaks when a session cookie expires mid-run. The post's rule is to run as many end-to-end tests as you can, as long as you can run them all in parallel with minimal flakes and automated upkeep.

Integration testing vs regression testing

Integration testing describes what a test checks, the connections between components, while regression testing describes why you run it, to confirm a change didn't break something that used to work. The two overlap, since integration tests that re-run on every change are part of the regression suite, along with unit and end-to-end tests.

Where do system testing and user acceptance testing fit?

System testing checks the complete, integrated application against its requirements, usually in a staging environment. User acceptance testing (UAT) comes last: the business or real users confirm the software does what they need before it's released. Integration testing comes before both, and developers usually own it.

How do you write integration tests?

Start with the connections that matter most, decide which dependencies are real and which are faked, give every test its own data, and run the suite automatically on every change.

  1. List the integration points. Every place your code crosses a boundary: calls between modules, service-to-service APIs, the database, queues, caches, and third parties such as payments, email, SMS and sign-in. Rank them by what breaks if they fail, with money, access and data first.
  2. Decide what's real. Run the dependencies you own for real, and use test doubles only for the ones you can't run reliably in a test.
  3. Set up the environment. Use a dedicated environment configured like production: containers for databases and queues on a developer's machine or in CI, and a shared staging environment or a preview environment per pull request for tests across services. Keep demos and manual testing out of it, since they change data under your tests.
  4. Give each test its own data. Create what a test needs through an API or a seed script, give it a unique ID or prefix so parallel runs don't collide, and clean it up afterward. A test that depends on another test's leftovers will fail at random.
  5. Write it as Arrange-Act-Assert. Set up the state, do one thing across the connection, then check both sides: the response, and what the other component did with it. QA Wolf's introduction to Arrange-Act-Assert covers the pattern.
  6. Test the failure paths. A dependency that's slow, down, rate-limited or returning errors; a retry that could charge a customer twice; a message delivered twice.
  7. Run them in CI. Run the fast integration tests on every pull request and the slower cross-service ones against staging after each deploy. A failure should block the merge or the release.

An example integration test with Playwright

Playwright is best known for browser tests, but its request fixture also sends HTTP requests directly, which makes it handy for API-level integration tests. This one checks the connection between an orders service and an inventory service: placing an order has to reserve stock, and an order for more than is in stock has to be rejected without reserving anything. Both tests follow Arrange-Act-Assert, and they assume baseURL in playwright.config.ts points at a test environment running both services.

import { test, expect } from '@playwright/test';
import { randomUUID } from 'node:crypto';

test('placing an order reserves stock in the inventory service', async ({ request }) => {
  // Arrange: a product with its own SKU, so parallel runs don't collide
  const sku = `test-${randomUUID()}`;
  const product = await request.post('/api/products', { data: { sku, stock: 5 } });
  expect(product.ok()).toBeTruthy();

  // Act: place an order through the orders service
  const order = await request.post('/api/orders', { data: { sku, quantity: 2 } });
  expect(order.status()).toBe(201);
  const { id } = await order.json();

  // Assert: the order was saved, and the inventory service reserved the stock
  const saved = await request.get(`/api/orders/${id}`);
  expect(await saved.json()).toMatchObject({ sku, quantity: 2, status: 'confirmed' });

  const stock = await request.get(`/api/products/${sku}`);
  expect(await stock.json()).toMatchObject({ stock: 3 });
});

test('an order larger than the stock is rejected and reserves nothing', async ({ request }) => {
  // Arrange: a product with one unit in stock
  const sku = `test-${randomUUID()}`;
  await request.post('/api/products', { data: { sku, stock: 1 } });

  // Act: order two units
  const order = await request.post('/api/orders', { data: { sku, quantity: 2 } });

  // Assert: the order is rejected, and no stock is reserved
  expect(order.status()).toBe(409);
  const stock = await request.get(`/api/products/${sku}`);
  expect(await stock.json()).toMatchObject({ stock: 1 });
});

Run it with npx playwright test. Each test makes its own product with a random SKU, so the two can run in parallel against a shared environment.

Should integration tests use mocks?

Use real versions of the dependencies you own, such as your database, queues and services, and mock only what you can't run reliably in a test, such as a third-party API with rate limits or one that charges money. Mocks make tests fast and predictable, but a mock only knows what you told it, so when the provider changes its API, the mocked test keeps passing.

A QA Wolf comparison of testing tools lists what mocking can miss: "tricky redirects and cookies, cross-site rules, data mismatches, slow servers, and side effects (like an email that never actually gets sent)." To close that gap, back each mocked API with a contract test, run a scheduled test against the provider's sandbox or test mode, and cover your user flows end to end against the real services.

What tools are used for integration testing?

Most teams run integration tests on the same framework as their unit tests, plus tools that start real dependencies and fake the ones they can't run.

  • JavaScript and TypeScript: Jest, Vitest or Mocha as the runner; Supertest or Playwright's API testing for HTTP requests; Mock Service Worker (MSW) or nock to fake outside APIs.
  • Java and Kotlin: JUnit 5 or TestNG; Spring Boot's test support; REST Assured for HTTP APIs; WireMock to fake outside services.
  • Python: pytest, with Django's or Flask's test client or httpx for HTTP; the responses or respx libraries to fake outside APIs.
  • .NET: xUnit or NUnit, with ASP.NET Core's WebApplicationFactory to run the app in memory.
  • Go: the standard library's testing and net/http/httptest packages.
  • Any stack: Testcontainers or Docker Compose to run real databases, queues and other services in containers; Pact for contract tests; Postman collections run with Newman for API checks.
  • Mobile: Flutter's integration_test package, Espresso on Android and XCUITest on iOS for in-app tests.

For integrations a user sees, such as a sign-up email or a checkout, end-to-end tools drive the real interface: Playwright for web apps and Appium for mobile apps.

Integration testing checklist

An integration is covered when:

  • Every integration point is on a list, ranked by risk.
  • Each test runs real versions of the dependencies you own.
  • Every mock of an outside API is backed by a contract test or a run against the provider's sandbox.
  • Each test creates its own data with a unique ID, and cleans up after itself.
  • Tests pass in any order and in parallel.
  • Failure paths are covered as well as the happy path: errors, timeouts, rate limits, retries and duplicate messages.
  • Assertions check both sides of the connection: the response, and the side effect (the record written, the email sent, the stock reserved).
  • The test environment is configured like production, feature flags included, and nothing else uses it during runs.
  • Tests run automatically in CI, and a failure blocks the merge or the release.
  • Someone investigates every failure before the next run, and flaky tests get fixed or removed.
  • Every user flow that crosses several integrations also has an end-to-end test, critical ones first.

How QA Wolf covers integrations with end-to-end tests

QA Wolf doesn't write integration tests in the sense above, where two components are checked against each other. Its AI agents write end-to-end tests that run each user flow through your app's real screens and APIs, so the flow exercises every integration it touches, including the email, SMS and third-party services behind it.

  • Email and SMS inside the test. QA Wolf tests send, receive and check email and SMS messages as steps in the flow: a sign-up confirmation, a magic link, a six-digit login code read from the inbox and typed back into the app. Each run can get a fresh inbox, so an email from an earlier run never matches, and QA Wolf runs its own email domains, which keeps these tests stable.
  • Sign-in with MFA. Tests generate the time-based one-time code an authenticator app would, so they log in through multi-factor authentication the way a user does.
  • APIs, test data and feature flags. QA Wolf's tests are deterministic Playwright and Appium code that calls APIs, seeds databases, mocks external dependencies and flips feature flags to exercise the app under real conditions.
  • Several users and devices in one run. Tests pass data to each other, so one user creates something and another acts on it, across web and mobile, in the same run.
  • On every pull request. PR testing runs the tests against each pull request's preview deploy and reports the result on the pull request, and QA Wolf runs the whole suite in parallel on its own testing infrastructure.

MakersPlace is a perfect example of what that covers. It had a robust suite of unit and integration tests but struggled to test, end to end, a flow that runs through a Chrome extension linked to a crypto wallet; QA Wolf built tests that install the extension, recognize bids, sales and token transfers, and verify each transaction on the blockchain. At GUIDEcx, email delivery used to be tested by hand, and QA Wolf now checks it each time a developer merges to staging or production. "I would definitely recommend QA Wolf for any software that has multiple modules that have to integrate together or have to work with third-party APIs," says Xima's VP of Engineering.

QA Wolf's guide to preparing test environments also works as a checklist for anyone's integration tests: run tests in a dedicated staging or test environment; keep databases, authentication providers, email services and queues available during runs; provide an API to create and clean up test users; prefix generated data with a run ID or timestamp; and aim for email and SMS delivery within a minute.

Want QA Wolf to test the flows that run through your integrations? Book a demo.

The bottom line

Integration testing checks the connections between the parts of your app, the place unit tests can't look. Test the connections you own against real dependencies, mock only what you must and back those mocks with contract tests or sandbox runs, give every test its own data, and run the suite on every change. Then cover every user flow end to end, critical ones first, because users go through every integration at once.

Frequently asked questions

  • What is integration testing?

    Integration testing checks that separately built parts of an application, such as modules, services, databases and third-party APIs, work correctly together. It sits between unit testing, which checks one piece in isolation, and end-to-end testing, which runs a whole user workflow.

  • What is an example of an integration test?

    A test that places an order through the orders API and checks that the inventory service reserved the stock is an integration test: it passes only if both services and the call between them work. Other examples are a sign-up that writes the user to the database and sends a confirmation email, and a checkout that charges a payment provider's test mode and marks the order paid.

  • What are the four types of integration testing?

    The four types are big bang, top-down, bottom-up and sandwich (also called hybrid). Big bang combines all the modules at once and tests them together. The other three are incremental: they add modules a few at a time, starting from the top, from the bottom, or from both ends.

  • What is the difference between unit testing and integration testing?

    A unit test checks one function or class in isolation, with its dependencies replaced by test doubles, and runs in milliseconds. An integration test checks that two or more components work together through their real connection, such as an API call or a database query. It's slower, and it catches mismatches a unit test can't see.

  • What's the difference between integration and regression testing?

    Integration testing is about what a test checks, the connections between components, while regression testing is about why you run tests, to confirm that a change didn't break something that used to work. Integration tests that re-run on every change are part of a regression suite.

  • Is integration testing done by QA?

    Usually developers write integration tests for the code they own, since the tests live alongside that code and run with each build. QA engineers and SDETs often own the broader tests that cross several services, along with system and end-to-end tests.

  • Is integration testing manual or automated?

    Mostly automated. Integration tests are code that runs in CI on every change, which a manual check can't keep up with. Teams still check a new third-party connection by hand in staging, then automate it.

  • Should integration tests use mocks?

    Use real versions of the dependencies you own, such as your database, queues and services, and mock only what you can't run reliably in a test, such as a rate-limited or paid third-party API. Back each mock with a contract test or a scheduled run against the provider's sandbox, because a mock keeps passing when the real API changes.

  • What tools are used for integration testing?

    Most teams use their unit test framework, such as Jest, Vitest, JUnit, pytest, xUnit or Go's testing package, plus tools for dependencies: Testcontainers or Docker Compose to run real databases and queues, WireMock or MSW to fake HTTP services, and Pact for contract tests. Playwright and Appium cover integrations end to end through the user interface.

  • Is API testing considered integration testing?

    Often, yes. An API test that calls a running service with its real database and dependencies is an integration test. A test of a single API handler with everything else mocked is closer to a unit test.

  • How do you do integration testing for microservices?

    Use contract tests between each consumer and provider to catch breaking API changes without starting every service. Add a smaller set of tests that run services together in a shared environment, and end-to-end tests for every user flow that crosses many services, critical ones first.

  • What is system integration testing?

    System integration testing (SIT) checks a complete system together with the other systems it exchanges data with, such as a CRM, an ERP or a payment gateway. It usually runs in a staging environment set up like production, after the components have been integration tested and before user acceptance testing.

  • Does QA Wolf test integrations with third-party services?

    Yes, end to end. QA Wolf's agents write end-to-end tests that run each user flow through your app's real UI and APIs, so they exercise the third-party services behind it. Tests can send, receive and check email and SMS messages, generate MFA codes, call APIs and seed test data.

More from the blog

All posts