Files
gh-tachyon-beep-skillpacks-…/skills/integration-testing-patterns/SKILL.md
2025-11-30 08:59:43 +08:00

12 KiB

name, description
name description
integration-testing-patterns Use when testing component integration, database testing, external service integration, test containers, testing message queues, microservices testing, or designing integration test suites - provides boundary testing patterns and anti-patterns between unit and E2E tests

Integration Testing Patterns

Overview

Core principle: Integration tests verify that multiple components work together correctly, testing at system boundaries.

Rule: Integration tests sit between unit tests (isolated) and E2E tests (full system). Test the integration points, not full user workflows.

Integration Testing vs Unit vs E2E

Aspect Unit Test Integration Test E2E Test
Scope Single function/class 2-3 components + boundaries Full system
Speed Fastest (<1ms) Medium (10-500ms) Slowest (1-10s)
Dependencies All mocked Real DB/services Everything real
When Every commit Every PR Before release
Coverage Business logic Integration points Critical workflows

Test Pyramid:

  • 70% Unit: Pure logic, no I/O
  • 20% Integration: Database, APIs, message queues
  • 10% E2E: Browser tests, full workflows

What to Integration Test

1. Database Integration

Test: Repository/DAO layer with real database

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

@pytest.fixture(scope="function")
def db_session():
    """Each test gets fresh DB with rollback."""
    engine = create_engine("postgresql://localhost/test_db")
    Session = sessionmaker(bind=engine)
    session = Session()

    yield session

    session.rollback()  # Undo all changes
    session.close()

def test_user_repository_create(db_session):
    """Integration test: Repository + Database."""
    repo = UserRepository(db_session)

    user = repo.create(email="alice@example.com", name="Alice")

    assert user.id is not None
    assert repo.get_by_email("alice@example.com").id == user.id

Why integration test:

  • Verifies SQL queries work
  • Catches FK constraint violations
  • Tests database-specific features (JSON columns, full-text search)

NOT unit test because: Uses real database NOT E2E test because: Doesn't test full user workflow


2. External API Integration

Test: Service layer calling third-party API

import pytest
import responses

@responses.activate
def test_payment_service_integration():
    """Integration test: PaymentService + Stripe API (mocked)."""
    # Mock Stripe API response
    responses.add(
        responses.POST,
        "https://api.stripe.com/v1/charges",
        json={"id": "ch_123", "status": "succeeded"},
        status=200
    )

    service = PaymentService(api_key="test_key")
    result = service.charge(amount=1000, token="tok_visa")

    assert result.status == "succeeded"
    assert result.charge_id == "ch_123"

Why integration test:

  • Tests HTTP client configuration
  • Validates request/response parsing
  • Verifies error handling

When to use real API:

  • Separate integration test suite (nightly)
  • Contract tests (see contract-testing skill)

3. Message Queue Integration

Test: Producer/Consumer with real queue

import pytest
from kombu import Connection

@pytest.fixture
def rabbitmq_connection():
    """Real RabbitMQ connection for integration tests."""
    conn = Connection("amqp://localhost")
    yield conn
    conn.release()

def test_order_queue_integration(rabbitmq_connection):
    """Integration test: OrderService + RabbitMQ."""
    publisher = OrderPublisher(rabbitmq_connection)
    consumer = OrderConsumer(rabbitmq_connection)

    # Publish message
    publisher.publish({"order_id": 123, "status": "pending"})

    # Consume message
    message = consumer.get(timeout=5)

    assert message["order_id"] == 123
    assert message["status"] == "pending"

Why integration test:

  • Verifies serialization/deserialization
  • Tests queue configuration (exchanges, routing keys)
  • Validates message durability

4. Microservices Integration

Test: Service A → Service B communication

import pytest

@pytest.fixture
def mock_user_service():
    """Mock User Service for integration tests."""
    with responses.RequestsMock() as rsps:
        rsps.add(
            responses.GET,
            "http://user-service/users/123",
            json={"id": 123, "name": "Alice"},
            status=200
        )
        yield rsps

def test_order_service_integration(mock_user_service):
    """Integration test: OrderService + UserService."""
    order_service = OrderService(user_service_url="http://user-service")

    order = order_service.create_order(user_id=123, items=[...])

    assert order.user_name == "Alice"

For real service integration: Use contract tests (see contract-testing skill)


Test Containers Pattern

Use Docker containers for integration tests.

import pytest
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="module")
def postgres_container():
    """Start PostgreSQL container for tests."""
    with PostgresContainer("postgres:15") as postgres:
        yield postgres

@pytest.fixture
def db_connection(postgres_container):
    """Database connection from test container."""
    engine = create_engine(postgres_container.get_connection_url())
    return engine.connect()

def test_user_repository(db_connection):
    repo = UserRepository(db_connection)
    user = repo.create(email="alice@example.com")
    assert user.id is not None

Benefits:

  • Clean database per test run
  • Matches production environment
  • No manual setup required

When NOT to use:

  • Unit tests (too slow)
  • CI without Docker support

Boundary Testing Strategy

Test at system boundaries, not internal implementation.

Boundaries to test:

  1. Application → Database (SQL queries, ORMs)
  2. Application → External API (HTTP clients, SDKs)
  3. Application → File System (File I/O, uploads)
  4. Application → Message Queue (Producers/consumers)
  5. Service A → Service B (Microservice calls)

Example: Boundary test for file upload

def test_file_upload_integration(tmp_path):
    """Integration test: FileService + File System."""
    service = FileService(storage_path=str(tmp_path))

    # Upload file
    file_id = service.upload(filename="test.txt", content=b"Hello")

    # Verify file exists on disk
    file_path = tmp_path / file_id / "test.txt"
    assert file_path.exists()
    assert file_path.read_bytes() == b"Hello"

Anti-Patterns Catalog

Testing Internal Implementation

Symptom: Integration test verifies internal method calls

# ❌ BAD: Testing implementation, not integration
def test_order_service():
    with patch('order_service._calculate_tax') as mock_tax:
        service.create_order(...)
        assert mock_tax.called

Why bad: Not testing integration point, just internal logic

Fix: Test actual boundary (database, API, etc.)

# ✅ GOOD: Test database integration
def test_order_service(db_session):
    service = OrderService(db_session)
    order = service.create_order(...)

    # Verify data was persisted
    saved_order = db_session.query(Order).get(order.id)
    assert saved_order.total == order.total

Full System Tests Disguised as Integration Tests

Symptom: "Integration test" requires entire system running

# ❌ BAD: This is an E2E test, not integration test
def test_checkout_flow():
    # Requires: Web server, database, Redis, Stripe, email service
    browser.goto("http://localhost:8000/checkout")
    browser.fill("#card", "4242424242424242")
    browser.click("#submit")
    assert "Success" in browser.content()

Why bad: Slow, fragile, hard to debug

Fix: Test individual integration points

# ✅ GOOD: Integration test for payment component only
def test_payment_integration(mock_stripe):
    service = PaymentService()
    result = service.charge(amount=1000, token="tok_visa")
    assert result.status == "succeeded"

Shared Test Data Across Integration Tests

Symptom: Tests fail when run in different orders

# ❌ BAD: Relies on shared database state
def test_get_user():
    user = db.query(User).filter_by(email="test@example.com").first()
    assert user.name == "Test User"

def test_update_user():
    user = db.query(User).filter_by(email="test@example.com").first()
    user.name = "Updated"
    db.commit()

Fix: Each test creates its own data (see test-isolation-fundamentals skill)

# ✅ GOOD: Isolated test data
def test_get_user(db_session):
    user = create_test_user(db_session, email="test@example.com")
    retrieved = db_session.query(User).get(user.id)
    assert retrieved.name == user.name

Testing Too Many Layers

Symptom: Integration test includes business logic validation

# ❌ BAD: Testing logic + integration in same test
def test_order_calculation(db_session):
    order = OrderService(db_session).create_order(...)

    # Integration: DB save
    assert order.id is not None

    # Logic: Tax calculation (should be unit test!)
    assert order.tax == order.subtotal * 0.08

Fix: Separate concerns

# ✅ GOOD: Unit test for logic
def test_order_tax_calculation():
    order = Order(subtotal=100)
    assert order.calculate_tax() == 8.0

# ✅ GOOD: Integration test for persistence
def test_order_persistence(db_session):
    repo = OrderRepository(db_session)
    order = repo.create(subtotal=100, tax=8.0)
    assert repo.get(order.id).tax == 8.0

Integration Test Environments

Local Development

# docker-compose.test.yml
version: '3.8'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: test_db
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test

  redis:
    image: redis:7

  rabbitmq:
    image: rabbitmq:3-management

Run tests:

docker-compose -f docker-compose.test.yml up -d
pytest tests/integration/
docker-compose -f docker-compose.test.yml down

CI/CD

# .github/workflows/integration-tests.yml
name: Integration Tests

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s

    steps:
      - uses: actions/checkout@v3
      - name: Run integration tests
        run: pytest tests/integration/
        env:
          DATABASE_URL: postgresql://postgres:test@localhost/test

Performance Considerations

Integration tests are slower than unit tests.

Optimization strategies:

  1. Use transactions: Rollback instead of truncating tables (100x faster)
  2. Parallelize: Run integration tests in parallel (pytest -n 4)
  3. Minimize I/O: Only test integration points, not full workflows
  4. Cache containers: Reuse test containers across tests (scope="module")

Example: Fast integration tests

# Slow: 5 seconds per test
@pytest.fixture
def db():
    engine = create_engine(...)
    Base.metadata.create_all(engine)  # Recreate schema every test
    yield engine
    Base.metadata.drop_all(engine)

# Fast: 10ms per test
@pytest.fixture(scope="module")
def db_engine():
    engine = create_engine(...)
    Base.metadata.create_all(engine)  # Once per module
    yield engine
    Base.metadata.drop_all(engine)

@pytest.fixture
def db_session(db_engine):
    connection = db_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)
    yield session
    transaction.rollback()  # Fast cleanup
    connection.close()

Bottom Line

Integration tests verify that components work together at system boundaries.

  • Test at boundaries (DB, API, queue), not internal logic
  • Use real dependencies (DB, queue) or realistic mocks (external APIs)
  • Keep tests isolated (transactions, test containers, unique data)
  • Run on every PR (they're slower than unit tests but faster than E2E)

If your "integration test" requires the entire system running, it's an E2E test. Test integration points individually.