Skip to content
Home
Integration Testing Guide: Database, API & Service Testing

Integration Testing Guide: Database, API & Service Testing

Testing & QA Testing & QA 8 min read 1522 words Beginner ExcellentWiki Editorial Team

Integration testing verifies that the components of a system work together correctly. While unit tests validate individual functions in isolation — using mocks to replace all dependencies — integration tests exercise real interactions between modules, databases, APIs, and external services. These tests catch interface mismatches that unit tests cannot detect because mocks are simplified representations of the real thing.

The ISTQB Foundation Level syllabus identifies integration testing as the second level of testing, positioned between unit and system testing. It focuses on interactions between integrated components and is essential for detecting defects in the interfaces, data flow, and control flow between modules. According to Software Testing: A Craftsman’s Approach by Paul Jorgensen, integration defects account for approximately 35 percent of all software defects in enterprise applications, making dedicated integration testing a high-return investment.

Why Integration Testing Matters

Mocks are always imperfect. A mock database might accept queries that the real database rejects. A mock API might return data in a format that differs from the production service. Integration tests expose these mismatches by using real or realistic implementations. The Google Testing Blog’s “Just Say No to More End-to-End Tests” article argues that most teams over-invest in fragile E2E tests while under-investing in integration tests, which offer a better cost-benefit ratio for catching interface defects.

What Integration Tests Catch

  • Interface mismatches — wrong parameter types, ordering, or data formats
  • Serialisation errors — JSON, XML, or protocol buffer encoding issues
  • State dependencies — assumptions about ordering of operations that do not hold
  • Configuration errors — missing environment variables, wrong connection strings
  • Timing issues — race conditions, deadlocks, timeout configurations

Integration tests also validate assumptions about third-party library behaviour. A library upgrade might change default encoding, alter connection pooling behaviour, or deprecate previously working APIs. Integration tests with real library versions catch these issues during development rather than in production.

Types of Integration Tests

Database Integration Tests

Database integration tests verify that the data access layer correctly reads, writes, and deletes records. These tests use real database instances — typically via Testcontainers — and apply schema migrations before execution.

import pytest
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="module")
def database():
    with PostgresContainer("postgres:16-alpine") as postgres:
        engine = create_engine(postgres.get_connection_url())
        run_migrations(engine)
        yield engine

def test_create_and_retrieve_user(database):
    user_repo = UserRepository(database)
    user_repo.create(email="alice@test.com", name="Alice")

    user = user_repo.find_by_email("alice@test.com")
    assert user is not None
    assert user.name == "Alice"

API Integration Tests

API integration tests verify that your service correctly handles HTTP requests and returns appropriate responses. These tests start the application server and send real HTTP requests to it.

def test_order_creation(client, database):
    # Create products first
    database.execute("INSERT INTO products (id, name, price) VALUES (1, 'Widget', 9.99)")

    response = client.post("/api/orders", json={
        "items": [{"product_id": 1, "quantity": 2}],
        "shipping_address": "123 Main St"
    })

    assert response.status_code == 201
    data = response.json()
    assert data["total"] == 19.98
    assert data["status"] == "pending"

External Service Integration Tests

Third-party services — payment processors, email providers, cloud APIs — require integration testing strategies that balance realism with reliability. Test sandboxes provided by the service vendor offer the most realistic validation. WireMock or MockServer can simulate service responses for scenarios that are difficult to trigger in sandboxes, such as timeouts and error responses.

A common pattern is the “circuit breaker” integration test: verify that your application behaves correctly when a third-party service returns 5xx errors, times out, or becomes completely unreachable. These tests validate that your application degrades gracefully rather than cascading failures to users. The book Building Microservices by Sam Newman dedicates significant attention to testing external integrations, recommending that teams maintain a “chaos testing” suite that deliberately introduces service failures to validate resilience.

Testcontainers for Realistic Dependencies

Testcontainers is a Java and Python library that manages disposable Docker containers for testing. It supports PostgreSQL, MySQL, Redis, Kafka, Elasticsearch, and dozens of other services.

from testcontainers.redis import RedisContainer
import redis

def test_cache_invalidation():
    with RedisContainer("redis:7-alpine") as redis_container:
        client = redis.Redis(
            host=redis_container.get_container_host_ip(),
            port=redis_container.get_exposed_port(6379)
        )
        client.set("key", "value")
        assert client.get("key") == b"value"
        client.delete("key")
        assert client.get("key") is None

Integration Test Strategies

The approach to integration testing depends on the system architecture and testing goals. The choice is often determined by team structure — teams that own a single service tend to prefer bottom-up integration, while teams that coordinate across multiple services often adopt sandwich integration for efficiency.

Big Bang Integration

All components are integrated simultaneously and tested as a whole. This approach is simple to set up but makes diagnosing failures difficult because any component could be the source of a defect. Big bang is best suited for small systems with fewer than five components.

Top-Down Integration

Testing begins with the top-level modules, and lower-level modules are stubbed. Stubs are gradually replaced with real implementations. This approach validates high-level business logic early but requires creating many stubs.

Bottom-Up Integration

Lower-level modules are tested first, then combined into progressively larger subsystems. This approach ensures that foundational components — database access, utility functions — are reliable before higher-level logic is integrated.

Sandwich Integration

A hybrid approach combining top-down and bottom-up strategies. Critical paths through the full stack are tested while non-critical components remain stubbed.

Continuous Integration Integration

Integration tests occupy the middle tier of the CI/CD pipeline. Run them on every pull request and before merging to the main branch. Use Docker Compose or Testcontainers to provision disposable test environments. Integration tests should complete within a few minutes — if they are slower, invest in parallelisation rather than reducing coverage. Tag integration tests with their dependency requirements so CI runners can be provisioned accordingly: database tests need database containers, message queue tests need message broker containers, and cloud API tests need network access to sandbox endpoints.

Best Practices

Use realistic data volumes. A database with ten rows performs differently from one with ten million rows — queries that work on small datasets may time out on production-scale data. Clean up state between tests using transaction rollback or truncation. Make test setup explicit — fixtures that hide important configuration make debugging difficult. Run integration tests regularly, not just before releases, to catch regressions early.

Consider the “test holiday” problem: when integration tests are slow, teams stop running them locally and only discover failures in CI. Keep integration test feedback under five minutes by parallelising and optimising the slowest tests. If a single integration test takes more than 30 seconds, it is likely testing too much — split it into smaller, focused tests.

Database Integration Testing

Test database interactions with a real database in a test environment, not with mocked database drivers. Use testcontainers to spin up disposable database instances in Docker containers for each test run. Seed test data using migrations and factories. Clean up between tests by truncating tables or wrapping each test in a transaction. Test edge cases: connection failures, constraint violations, race conditions, and large datasets.

Testing External APIs

When your application depends on external APIs, use wiremock or MockServer to simulate the external service. Define stub responses for happy paths, error codes, timeouts, and unexpected payloads. Verify that your application handles each response type correctly. Contract testing (using Pact) ensures your assumptions about the external API align with the actual provider.

Test Environment Management

Test environments are a common bottleneck in quality assurance. Challenges: environment availability conflicts, configuration drift between environments, data inconsistency, and recreation time. Solutions: use infrastructure-as-code (Terraform, Pulumi, CloudFormation) to provision environments consistently. Containerize test environments with Docker Compose for reproducibility. Use ephemeral environments — spin up for each test run and destroy afterward. Production-like test data is essential: anonymized production data copies, synthetic data generation tools, and data masking for sensitive information. Database migration testing is often overlooked — test that migrations work both forward and backward. Environment monitoring tracks availability and alerts when issues arise.

Exploratory Testing Techniques

Exploratory testing complements automated testing by finding unexpected issues. Session-based test management structures exploratory testing into timed sessions with a charter (mission), notes, and bug reports. Heuristic testing uses rules of thumb to guide exploration: FEW HICCUPS (Functionality, Errors, Workflow, High-volume, Interrupts, Compatibility, Configuration, Usability, Performance, Security). Pair testing — two testers working together — combines different perspectives and catches more issues than solo testing. Charter-based testing defines a mission without detailed test cases, allowing testers to follow their curiosity within bounds.

FAQ

Q: What is the difference between integration testing and end-to-end testing?
A: Integration testing validates component interactions at the service level. End-to-end testing validates complete user workflows through the entire stack.

Q: Should I use Testcontainers or mocks for database testing?
A: Use Testcontainers for schema validation and query correctness. Use mocks for testing error-handling logic when the database is unavailable.

Q: How many integration tests do I need?
A: Cover each significant component interaction. Focus on critical paths — data persistence, API contracts, external service communication. Quality matters more than quantity.

Q: What is the best way to test external API integrations?
A: Use vendor sandboxes for realistic testing. Use WireMock for simulating edge cases like timeouts and error responses.

Internal Links

  • Read Testing Fundamentals to understand where integration tests sit in the testing pyramid.
  • Compare integration testing with End-to-End Testing to decide the right coverage level for your system.
  • Learn how API Testing Tools support integration testing of REST and GraphQL services.
Section: Testing & QA 1522 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top