Programming

Integration Testing: Verify Real Boundaries Between Components

tuyenpham
October 5, 2026 schedule 5 min read
integration testing: process overview

Integration testing checks how components work together across a real boundary. A unit test can verify a function in isolation, while an integration test can show whether the application writes to a database correctly, serializes an HTTP response as intended, or applies a migration successfully. The useful question is which contract could fail even if each individual component appears correct. Focus on those contracts and control the test environment so failures identify a product problem rather than unpredictable shared state.

integration testing: a practical workflow

The sequence is Prepare state → Exercise boundary → Assert outcome → Clean resources. The sections below explain what each decision changes, which failure cases deserve attention, and how to verify the result in a small project. Use the example as a starting point, then adapt the interfaces and error policy to your own application.

Choose the boundary and the level of realism

integration testing: overview
Overview

A database-backed API test may run the real router, validation, service, and database while substituting an external payment service. This is still useful integration coverage; every dependency does not have to be live. Use the same database engine as production when SQL syntax, constraints, or transaction behavior matter. An in-memory substitute can hide differences. Write down which components are real and which are controlled doubles so reviewers understand the evidence the test actually provides.

Create isolated data and explicit fixtures

Each test should create the state it needs instead of depending on another test running first. Use unique identifiers, an isolated schema, a disposable database, or another deliberate cleanup strategy. Transaction rollback can be convenient, but it does not isolate work happening through a different connection outside that transaction. Fixture scope matters: a session-level resource can reduce setup cost while still requiring per-test data isolation. Always release resources even when an assertion fails, so one failure does not poison later tests.

Assert public behavior and durable effects

integration testing: checklist
Checklist

For a create endpoint, assert the response status, response shape, and persisted record. For a rejected request, assert both the error contract and the absence of unintended writes. Avoid coupling every assertion to internal method call order when the purpose is to verify integration behavior. Include constraints, migrations, encoding, and authorization paths that unit tests cannot represent reliably. One successful request is insufficient when the highest risk comes from duplicate input, rollback, or concurrent activity.

Make failures reproducible in continuous integration

Pin dependencies and use health checks rather than assuming a database will be ready after an arbitrary sleep. Apply migrations in a consistent way and capture useful logs on failure. Keep secrets and production credentials out of fixtures. Tests that depend on current time should use a controlled clock where possible, and external network calls should be explicitly managed. A flaky test erodes confidence, so investigate its state, ordering, timing, and resource assumptions instead of immediately adding retries.

Example and implementation notes

def test_create_task(client, database):
    response = client.post("/tasks", json={"title": "Review"})
    assert response.status_code == 201
    task_id = response.json()["id"]
    stored = database.find_task(task_id)
    assert stored.title == "Review"

def test_invalid_task_does_not_persist(client, database):
    before = database.count_tasks()
    response = client.post("/tasks", json={"title": ""})
    assert response.status_code == 422
    assert database.count_tasks() == before

This example illustrates the core contract rather than a complete production application. Connect it to your real inputs, authentication rules, and failure-handling paths before using it. Check the expected output first, then change one input at a time to verify the assumptions. Keep those checks alongside the implementation so future changes do not silently weaken the behavior.

integration testing: workflow
Workflow

Verification checklist

  • Run tests individually and in a different order.
  • Confirm fixtures use a disposable environment.
  • Verify failure paths leave no partial writes.
  • Capture startup and database logs when CI fails.

Practice exercise

Add two integration tests to a small task API: one for successful creation and one for invalid input. Use a disposable instance of the production database engine and apply migrations before testing. Run the tests repeatedly and in parallel if your fixture design supports it. Then intentionally break a column constraint or response serializer to confirm the tests catch a failure beyond the isolated function level. Document which external services remain substituted.

integration testing: mistakes
Mistakes

Review the result

After completing the exercise, describe the successful path and one failure path in plain language. Record the input, expected result, actual result, and the evidence you used to compare them. A passing demonstration should remain repeatable after a dependency update or a refactor. If you cannot reproduce the result, reduce the example until the source of uncertainty is visible. Keep the smallest useful regression check rather than depending on memory.

References and related reading

Consult the official reference for integration testing for exact platform behavior. Continue with a practical debugging workflow and unit testing basics to turn these decisions into repeatable checks.

Share Article: share

Discussion

Join the conversation

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Articles