Skip to content
Integration Testing: Strategies, Patterns, and Best Practices

Integration Testing: Strategies, Patterns, and Best Practices

DodaTech Updated Jun 20, 2026 7 min read

Integration testing verifies that multiple software modules or components work together correctly — it bridges the gap between fast-but-isolated unit tests and slow-but-comprehensive end-to-end tests.

What You’ll Learn

  • Integration testing strategies: big bang, top-down, bottom-up, and sandwich
  • How to set up database integration tests with test containers
  • Contract testing with Pact for microservice interactions
  • When to use real dependencies vs mocks in integration tests
  • How to organize and maintain an integration test suite

Why Integration Testing Matters

Unit tests prove individual components work in isolation. But components that each work perfectly in isolation can fail when connected — wrong data shapes, missing fields, incorrect error propagation, timing issues. Integration tests catch these mismatches. A study by Google found that 35% of production bugs were integration issues that unit tests missed entirely. Integration testing is essential for catching these failures before they reach users.

Durga Antivirus Pro runs integration tests between signature databases, scan engines, and reporting services — a data format mismatch between any two components could cause missed detections.

Learning Path

    flowchart LR
  A[Unit Testing] --> B[Integration Testing<br/>You are here]
  B --> C[Contract Testing]
  C --> D[E2E Testing]
  D --> E[CI/CD Pipeline]
  style B fill:#f90,color:#fff
  

Integration Testing Strategies

Big Bang

All components are integrated at once and tested together.

Pros: Simple to plan, tests the full system. Cons: Hard to isolate failures. When the test fails, you don’t know which component caused it.

Top-Down

Test high-level components first, stubbing lower-level ones. Gradually replace stubs with real implementations.

Pros: Validates major workflows early. Cons: Lower-level components get tested later.

Bottom-Up

Test low-level components first, then compose them into higher-level tests.

Pros: Core infrastructure is validated first. Cons: No user-facing workflow testing until late.

Sandwich (Hybrid)

Combine top-down and bottom-up — test the UI layer and data layer simultaneously, meeting in the middle.

Pros: Balanced coverage. Cons: More complex to orchestrate.

Most teams use a hybrid approach: bottom-up for data access layers, top-down for API and UI layers.

Database Integration Testing

Testing database interactions requires careful setup. Use testcontainers for isolated, repeatable database tests:

# test_user_repository.py
import pytest
from testcontainers.postgres import PostgresContainer
from user_repository import UserRepository

@pytest.fixture(scope="module")
def postgres():
    with PostgresContainer("postgres:16") as pg:
        yield pg

@pytest.fixture
def repo(postgres):
    # Set up connection and schema
    connection_url = postgres.get_connection_url()
    repo = UserRepository(connection_url)
    repo.migrate()  # Create tables
    yield repo
    repo.cleanup()  # Drop tables

def test_insert_and_find_user(repo):
    user = repo.create_user("alice@test.com", "Alice")
    found = repo.find_by_email("alice@test.com")
    assert found.id == user.id
    assert found.name == "Alice"

def test_returns_none_for_missing_user(repo):
    result = repo.find_by_email("missing@test.com")
    assert result is None

def test_prevents_duplicate_email(repo):
    repo.create_user("bob@test.com", "Bob")
    with pytest.raises(IntegrityError):
        repo.create_user("bob@test.com", "Bob Again")

Key Principles

  • Fresh database per test run: Never share database state between tests
  • Clean up after each test: Drop or truncate tables
  • Use transactions: Wrap each test in a transaction and roll back
  • Speed matters: Keep schema light and data minimal

Contract Testing with Pact

Contract testing verifies that two services (consumer and provider) agree on the API contract between them. Pact is the most popular contract testing framework:

// consumer-side test (frontend service)
const { Pact } = require('@pact-foundation/pact');
const API = require('./api');

const provider = new Pact({
  consumer: 'WebFrontend',
  provider: 'UserService',
});

beforeAll(() => provider.setup());
afterEach(() => provider.verify());
afterAll(() => provider.finalize());

test('get user returns expected data', async () => {
  await provider.addInteraction({
    state: 'user alice exists',
    uponReceiving: 'a request for user alice',
    withRequest: {
      method: 'GET',
      path: '/users/alice',
    },
    willRespondWith: {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
      body: {
        id: 1,
        name: 'Alice',
        email: 'alice@test.com',
      },
    },
  });

  const user = await API.getUser('alice');
  expect(user.name).toBe('Alice');
});

The consumer generates a pact file that is shared with the provider. The provider runs its own tests to verify it can satisfy the contract:

# provider-side test (UserService)
from pact import Verifier

def test_user_service_contract():
    verifier = Verifier(provider="UserService")
    verifier.verify_with_broker(
        broker_url="https://pact-broker.example.com",
        provider_version="1.0.0",
    )

API Integration Testing

Test REST endpoints with real HTTP calls:

# test_api_integration.py
import pytest
import requests

BASE_URL = "http://localhost:8000/api"

class TestUserAPI:
    def test_create_user(self):
        response = requests.post(
            f"{BASE_URL}/users",
            json={"name": "Alice", "email": "alice@test.com"}
        )
        assert response.status_code == 201
        data = response.json()
        assert data["name"] == "Alice"
        assert "id" in data

    def test_get_user(self):
        # Create first
        create_resp = requests.post(
            f"{BASE_URL}/users",
            json={"name": "Bob", "email": "bob@test.com"}
        )
        user_id = create_resp.json()["id"]

        # Then retrieve
        response = requests.get(f"{BASE_URL}/users/{user_id}")
        assert response.status_code == 200
        assert response.json()["name"] == "Bob"

    def test_get_nonexistent_user(self):
        response = requests.get(f"{BASE_URL}/users/99999")
        assert response.status_code == 404

Message Queue Integration

Test interactions with message brokers:

# test_event_publisher.py
import json
from event_publisher import EventPublisher

def test_publishes_user_registered_event():
    publisher = EventPublisher(host="localhost", port=5672)
    channel = publisher.connect()

    # Set up test consumer
    messages = []
    def callback(ch, method, properties, body):
        messages.append(json.loads(body))

    channel.basic_consume(
        queue="test_queue",
        on_message_callback=callback,
        auto_ack=True
    )

    # Publish event
    publisher.publish("user.registered", {"user_id": 1, "email": "test@test.com"})

    # Verify message received
    assert len(messages) == 1
    assert messages[0]["user_id"] == 1
    assert messages[0]["email"] == "test@test.com"

Common Integration Testing Mistakes

1. Using In-Memory Databases for Tests

SQLite in-memory behaves differently from PostgreSQL in production. You’ll pass tests but hit production bugs.

Fix: Use testcontainers or a real database instance matching production.

2. Shared Test Data

Tests that depend on each other’s data produce non-deterministic failures.

Fix: Each test sets up its own data. Clean up after each test.

3. Testing Through the UI

End-to-end browser tests are not integration tests. They’re slower and more brittle.

Fix: Test at the API layer for integration. Reserve E2E for critical user journeys.

4. Ignoring Network Failures

Integration tests that always succeed mask network issues.

Fix: Test timeout handling, retry logic, and graceful degradation.

5. Too Many End-to-End Tests Mistaken as Integration

A test that hits the database AND an external API AND a browser is E2E, not integration.

Fix: Each integration test should test one integration point, not the whole chain.

6. No Test for Error Propagation

Test what happens when a downstream service returns 500 or times out.

Fix: Test both happy paths and error paths for every integration point.

7. Flaky Tests Due to Timing

Async operations with hardcoded timeouts produce flaky tests.

Fix: Use retry mechanisms with backoff, or poll for expected state.

Practice Questions

1. What is the difference between big bang and incremental integration testing?

Big bang integrates everything at once; incremental integrates one component at a time. Incremental makes failure isolation easier.

2. Why should integration tests use real databases instead of in-memory substitutes?

In-memory databases have different behavior — SQL features, transaction isolation, locking — that mask production bugs.

3. What is contract testing and when would you use it?

Contract testing verifies that a consumer and provider agree on API contracts. Use it in microservice architectures where services are developed independently.

4. How do you ensure integration tests are isolated?

Each test creates its own data, uses its own transaction (rolled back after), and does not depend on other tests’ state.

5. What should you test beyond the happy path in integration tests?

Error handling — timeouts, 500 errors, invalid data, missing resources, and network failures.

Challenge: Set up a testcontainers-based integration test suite for a microservice that connects to PostgreSQL and Redis. Implement tests for data persistence, cache invalidation, and error scenarios.

FAQ

How many integration tests should I have?
Aim for roughly 20% of your test suite to be integration tests, following the test pyramid. Fewer than unit tests, more than E2E tests.
Should integration tests run in CI?
Yes, but they’re slower than unit tests. Run them in a separate CI job after unit tests pass, or use parallelization.
How do I handle flaky integration tests?
Common causes: shared state, timing, network dependencies. Fix by ensuring isolation, using retries with backoff, and running against deterministic data.
Can I mock external APIs in integration tests?
Yes, using tools like WireMock or MockServer. This tests your code’s interaction with the API without depending on the API’s availability.
What is the difference between integration and functional testing?
Integration testing focuses on component interaction. Functional testing focuses on whether a feature works end-to-end. There’s overlap, but the emphasis differs.

What’s Next

TutorialWhat You’ll Learn
Contract Testing with PactFormal contract testing for microservices
End-to-End Testing GuideBrowser-level testing with Playwright
Test Automation FrameworksComparing testing tools and frameworks

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro. Updated 2026-06-20.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro