KAHIBARO
Discord Login Register

19.3. Integration Testing

Why Integration Testing Matters

Unit tests focus on small pieces of code in isolation. Integration tests focus on how pieces work together.

In backend development, most real bugs appear when different parts of the system interact, for example:

Integration testing helps you catch these problems before they appear in production.

Definition:
Integration testing checks that multiple components of your system work correctly together, using realistic inputs and infrastructure, such as a real or test database and real HTTP requests.

Unit tests ask:

"Does this function return the right result for this input?"

Integration tests ask:

"Given a running API and a database, does this endpoint behave correctly end to end?"

Unit Tests vs Integration Tests

You should already understand unit tests in this course, so here we only contrast them with integration tests.

Levels of Testing in a Backend

A simplified view:

LevelWhat it testsSpeedTypical tools
Unit testsSingle function / methodVery fastpytest, unittest
Integration testsMultiple components working togetherMediumpytest + HTTP client + test DB
End to end testsWhole system from a user perspectiveSlowBrowser tests, API tests with real deps

Integration tests live between unit tests and full end to end tests.

What Integration Tests Usually Include

An integration test for a backend often involves:

Examples:

What Integration Tests Look Like for Web Backends

Typical Integration Test Scenario

A classic backend integration test checks: HTTP request β†’ application β†’ database β†’ HTTP response.

Example scenario in plain language:

  1. Start your API app in a test mode.
  2. Ensure the test database is empty.
  3. Send an HTTP POST /users request with JSON data.
  4. Assert:
    • HTTP status code is 201 Created.
    • JSON response contains the right fields.
  5. Query the test database directly.
  6. Assert:
    • A new user exists with the expected values.
    • Password was hashed, not stored in plain text.

This is already far beyond what any single unit test can guarantee.

Important Integration Test Properties

Integration tests are:

When to Write Integration Tests

You do not need an integration test for every function. Focus on critical flows and boundaries.

Good Candidates for Integration Tests

These areas benefit a lot from integration testing:

AreaExample integration tests

|-------------------------------|-------------------------------------------------------------------------|

Authentication & sessionsLogin endpoint, token creation, token validation
Database access & transactionsCreate, update, delete flows for important entities
Validation & error responsesInvalid payloads, missing fields, invalid types
Security critical behaviorAccess control, permission checks, resource ownership
Cross-component workflowsCreating an order updates inventory and records a payment entry

If a bug would be very harmful in production (security, money, data loss), it is usually worth an integration test.

When a Unit Test Is Enough

Prefer a unit test when:

You can often do both:

Designing Integration Tests

Black Box vs White Box

You can treat your backend as a:

Most backend integration tests are black box from the HTTP side, white box about the database:

Arrange, Act, Assert Pattern

Structure each test with three steps:

  1. Arrange
    Set up test data and environment. For example, create a user in the database.
  2. Act
    Perform the primary action. For example, send an HTTP request to /login.
  3. Assert
    Check that the observable results match expectations. For example, token returned, status code is 200, database updated.

Example in pseudocode:

python
def test_login_success(integration_client, db_session):
    # Arrange
    create_user(db_session, email="test@example.com", password="correct_password")
    # Act
    response = integration_client.post("/login", json={
        "email": "test@example.com",
        "password": "correct_password",
    })
    # Assert
    assert response.status_code == 200
    data = response.json()
    assert "access_token" in data

Test Only One Scenario per Test

Each integration test should check one clear scenario:

If you combine many different error cases into a single test, it becomes hard to maintain and debug.

Integration Testing Strategy in Python Backends

You will see concrete tools in later chapters, especially when testing FastAPI. Here we focus on patterns, not specific libraries.

Typical Test Stack

A common setup for Python backend integration tests:

ConcernExample choice (you will see details later)
Test runnerpytest
HTTP clientFramework test client or httpx
Test databasePostgreSQL instance or in memory SQLite (temporary)
Database cleanupFixtures or database transactions per test
Test configurationSeparate config for tests (test DB, no emails, etc.)

Separate Configuration for Tests

Your application will have configuration for:

For integration tests you usually have separate values, for example:

EnvironmentDB nameEmail sendingLogging level
Developmentapp_devMaybe real or sandboxDEBUG
Test (integration tests)app_testDisabled or fakeWARNING
Productionapp_prodRealINFO

Rule:
Never run integration tests against your production database or services.
Always use a separate test environment, preferably isolated and disposable.

This prevents real data loss, spam emails to users, or charges on real payment providers.

Handling Databases in Integration Tests

Most backend integration tests talk to a database. Handling it correctly is critical.

Using a Separate Test Database

Always point integration tests to a separate database instance or schema.

Example pattern:

Your test setup:

  1. Reads TEST_DATABASE_URL.
  2. Creates the database or clears existing data.
  3. Runs migrations to create tables.
  4. Uses this database for all tests.

You can do this once per test session to reduce overhead.

Keeping Tests Independent

Integration tests often change database state. You must keep tests independent, so one test does not break another.

Common approaches:

ApproachIdeaProsCons
Truncate tables between testsAfter each test, delete all rows from all tablesSimple to understandCan be slow
Use DB transactions per testStart a transaction, run the test, then roll backVery fastNeeds careful setup
Create a fresh DB per test classCreate a new temporary DB or schema for a group of tests (test class)Good isolation between groupsMore complex infrastructure

A typical pattern:

This makes integration tests more deterministic and easier to run in any order.

Testing Transactions and Constraints

Integration tests are perfect for checking:

Example scenarios:

You write an integration test that:

  1. Calls the endpoint or service that performs the transaction.
  2. Asserts the HTTP response is correct.
  3. Queries the database to check that data is exactly as expected, including no partial writes.

Testing HTTP APIs with Integration Tests

Since this course focuses on backend APIs, most integration tests will call HTTP endpoints.

Using a Test HTTP Client

Your framework will provide or support an HTTP client that can talk to your app in a test environment. The details differ, but the pattern is similar:

Example shape in pseudocode:

python
def test_get_tasks_returns_list(client):
    response = client.get("/tasks")
    assert response.status_code == 200
    data = response.json()
    assert isinstance(data, list)

For tests that change state:

python
def test_create_task_persists_in_db(client, db_session):
    # Act
    response = client.post("/tasks", json={"title": "Test task"})
    assert response.status_code == 201
    task_id = response.json()["id"]
    # Assert in DB
    task = db_session.get(Task, task_id)
    assert task is not None
    assert task.title == "Test task"

Testing Error Handling

Integration tests are very useful for checking how errors are exposed to clients.

Examples:

You design tests such as:

python
def test_create_task_without_title_returns_422(client):
    response = client.post("/tasks", json={})
    assert response.status_code == 422
    data = response.json()
    # Exact structure depends on your framework
    assert "errors" in data

This ensures that clients of your API get predictable, documented errors.

Testing Authentication and Authorization

Integration tests can simulate:

Typical patterns:

Example structure:

python
def test_get_profile_requires_auth(client):
    response = client.get("/me")
    assert response.status_code == 401
def test_get_profile_returns_user_data(client, user_token):
    response = client.get("/me", headers={"Authorization": f"Bearer {user_token}"})
    assert response.status_code == 200
    data = response.json()
    assert data["email"] == "user@example.com"

Working with External Dependencies

Backends rarely live alone. They send emails, call other services, and integrate with queues. Integration tests must address this.

Real Services vs Test Doubles

You often have two options for each external dependency:

  1. Use the real service
    For example, a local Redis instance or a local message queue.
  2. Use a test double
    For example, an in memory fake implementation, or a mock.

A balanced strategy:

Fakes vs Mocks in Integration Tests

Terminology:

Integration tests often use fakes more than detailed mocks, because we want to test the interaction between many real components.

Example:

Pseudo pattern:

python
def test_registration_sends_verification_email(client, fake_email_service):
    response = client.post("/register", json={
        "email": "new@example.com",
        "password": "secret123"
    })
    assert response.status_code == 201
    assert len(fake_email_service.sent_emails) == 1
    assert fake_email_service.sent_emails[0].to == "new@example.com"

This keeps tests deterministic and fast, while still checking that integration with the email service works logically.

Organizing Integration Tests

As your project grows, you need a clear structure for integration tests.

Separating Unit and Integration Tests

You can separate tests by:

Then you can run them separately, for example:

Grouping by Feature or Module

Within tests/integration/, you can group by domain feature:

Inside each file, structure tests by endpoint or scenario:

python
# tests/integration/test_auth.py
def test_register_creates_user(...):
    ...
def test_register_duplicate_email_returns_400(...):
    ...
def test_login_success(...):
    ...

This mirrors how your API is organized and makes tests easier to navigate.

Using Fixtures for Setup and Teardown

Integration tests often share setup logic, such as:

You can use test fixtures (for example, pytest fixtures) to:

Although we will not go deep into fixture code here, you will see patterns like:

Integration Testing in Continuous Integration (CI)

Integration tests are an important part of CI pipelines.

Where Integration Tests Fit in CI

A typical pipeline:

  1. Run linting tools.
  2. Run unit tests.
  3. Run integration tests.
  4. Build Docker images.
  5. Deploy to a test or staging environment.

You can choose:

Infrastructure in CI

To run integration tests in CI, you need infrastructure:

Common approaches:

Your test code then uses TEST_DATABASE_URL pointing to the CI database container.

This allows integration tests to run in the same way locally and in CI, which increases reliability.

Common Pitfalls and Best Practices

Pitfalls

Here are common mistakes in integration testing:

PitfallProblem
Using production resourcesRisk of data loss, spam, costs
Tests depend on each otherOrder dependent tests, flaky results
Too much mockingIntegration tests become similar to unit tests, lose their value
No cleanup of shared stateDatabase or cache polluted, tests interfere with each other
Oversized testsVery long tests that cover too many cases, hard to debug and maintain
Ignoring non happy pathsOnly success flow tested, errors in real use cause crashes

Best Practices

Some useful rules of thumb:

Key rules for integration tests:

  1. Use a separate test environment and never touch production.
  2. Keep tests independent and repeatable. They should pass in any order.
  3. Focus on important flows and boundaries, not every small detail.
  4. Test both happy paths and failure paths for critical features.
  5. Automate running integration tests in your CI pipeline.

Additional tips:

Summary

Integration testing verifies that multiple parts of your backend, such as HTTP endpoints, middleware, business logic, and the database, work together correctly.

You saw:

In later chapters, especially when testing FastAPI, you will apply these ideas with real code, HTTP clients, and test databases.

Views: 6

Comments

Please login to add a comment.

Don't have an account? Register now!