Skip to main content

Component and Integration Testing

Test service boundaries, API contracts, and data flows with reproducible component and integration tests using test containers and contract testing frameworks.

TL;DR

Component tests verify a single service/component in isolation with its real dependencies (databases, caches) replaced by test doubles. Integration tests verify multiple components interact correctly across process boundaries (HTTP APIs, messaging). Use test containers for reproducible DB/external service setup, contract tests for API compatibility, and test data builders to avoid brittle fixtures. Target 20-50% integration test coverage; layer with fast unit tests (70%) and slow E2E tests (10-20%) for efficient feedback.

Learning Objectives

By the end of this article, you'll understand:

  • Component vs integration vs unit testing trade-offs
  • Test isolation strategies and avoiding flaky tests
  • API contract testing for backward compatibility
  • Database integration testing best practices
  • Test containers for reproducible external dependencies
  • Test data management and avoiding duplication

Motivating Scenario

Your microservice updated its database schema, breaking the downstream payment service integration. The integration tests passed because they tested in isolation. A new team member adds API parameters that weren't documented; they work locally but fail in staging. Your test database grows to 5GB with duplicate test data, making CI jobs timeout. You need integration tests that catch these real failures, run reliably in CI, and stay fast enough not to become a bottleneck.

Core Concepts

Component Testing

Tests a single service with real dependencies (database, cache) but mocked external services (other microservices, third-party APIs). Verifies business logic and database interactions within a service boundary.

Characteristics:

  • Tests one service/component in isolation
  • Uses test database, real ORM, real storage logic
  • Mocks external services (HTTP, messaging)
  • Faster than E2E (seconds to minutes)
  • Catches business logic, database schema issues
  • Deterministic and reproducible

Example: Testing order service with real PostgreSQL but mocked payment service.

Integration Testing

Verifies multiple components interact correctly across process boundaries (APIs, message queues, databases). Tests the contracts between services.

Characteristics:

  • Tests 2-3 loosely coupled components
  • May involve real HTTP calls or message brokers
  • Tests API contracts (request/response formats)
  • Detects serialization, naming, version mismatches
  • Slower than component tests (seconds)
  • Requires isolated test environments

Example: Order service calls payment service; verify request payload matches API contract.

Test Containers

Docker containers that provide ephemeral databases and services for testing. Each test gets a fresh instance; no state pollution between tests.

Benefits:

  • Identical schema to production
  • No test data setup complexity
  • Parallel test execution (each gets own container)
  • Tests real driver logic (JDBC, TCP protocol)
  • Portable (same setup locally and CI)

API Contract Testing

Consumer-driven contracts specify API expectations (request format, response schema, status codes). Caught mismatches before deployment.

Patterns:

  • Provider tests (service verifies it meets contracts)
  • Consumer tests (client verifies expected API shape)
  • Bi-directional contracts prevent surprises
  • Can run without actual services running

Practical Example

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@Testcontainers
class OrderServiceComponentTest {

@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>()
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");

private OrderRepository orderRepository;
private OrderService orderService;
private PaymentClient paymentClientMock;

@BeforeEach
void setup() {
// Setup real database connection from container
DataSource dataSource = createDataSource(
postgres.getJdbcUrl(),
postgres.getUsername(),
postgres.getPassword()
);

orderRepository = new PostgresOrderRepository(dataSource);

// Mock external service
paymentClientMock = mock(PaymentClient.class);

orderService = new OrderService(
orderRepository,
paymentClientMock
);
}

@Test
void testCreateOrderWithValidPayment() {
// Arrange
CreateOrderRequest request = new CreateOrderRequest(
userId = 123,
items = [new OrderItem(productId = 1, quantity = 2)]
);

when(paymentClientMock.charge(any()))
.thenReturn(new PaymentResponse(success = true, id = "payment-1"));

// Act
Order order = orderService.createOrder(request);

// Assert
assertNotNull(order.getId());
assertEquals("PENDING", order.getStatus());

// Verify payment was called with correct amount
ArgumentCaptor<ChargeRequest> captor =
ArgumentCaptor.forClass(ChargeRequest.class);
verify(paymentClientMock).charge(captor.capture());

ChargeRequest captured = captor.getValue();
assertEquals(100.0, captured.getAmount()); // 2 items * 50 each

// Verify data persisted in real database
Order persisted = orderRepository.findById(order.getId()).get();
assertEquals("PENDING", persisted.getStatus());
}

@Test
void testCreateOrderWithFailedPayment() {
CreateOrderRequest request = new CreateOrderRequest(
userId = 123,
items = [new OrderItem(productId = 1, quantity = 1)]
);

when(paymentClientMock.charge(any()))
.thenThrow(new PaymentException("Card declined"));

// Act & Assert
assertThrows(PaymentException.class, () ->
orderService.createOrder(request)
);

// Verify order was NOT persisted
assertTrue(orderRepository.findAll().isEmpty());
}
}

When to Use / When Not to Use

Component Test When:
  1. Testing complex business logic within a service
  2. Database interactions and schema changes
  3. Need fast feedback (< 1 second per test)
  4. Mocking external services (payment, email)
  5. Testing error handling and retries
Integration Test When:
  1. Testing API contracts between services
  2. Multi-service workflows (order -> payment -> shipping)
  3. Message queue interactions
  4. Cache invalidation across services
  5. Testing rare failure scenarios (timeout, disconnection)

Patterns & Pitfalls

Design Review Checklist

  • Component tests cover critical business logic (80%+ branch coverage)
  • Test databases use TestContainers or equivalent for reproducibility
  • Each test is independent (no shared state between tests)
  • Test data created with builders, not hard-coded fixtures
  • Integration tests verify API contracts (request/response formats)
  • Mocks used only for external services (not internal components)
  • Database migrations run in test setup, not committed test data
  • Tests run in parallel without interference
  • Component tests run in < 5 seconds; integration in < 10 seconds
  • Documentation explains test strategy (unit/component/integration ratio)

Self-Check

Ask yourself:

  • Can I run all component tests without a test database running first?
  • If my database schema changes, do my tests still pass?
  • Do I have tests for the APIs my service exposes?
  • Are my tests deterministic (same result every run)?
  • Can new team members run tests without setup instructions?

One Key Takeaway

info

Component and integration tests fill the gap between brittle unit tests and slow E2E tests. Use TestContainers for reproducible dependencies, contract tests for API compatibility, and test data builders to avoid brittle fixtures. The goal is to catch real failures (schema changes, API mismatches) with fast, reliable tests.

Next Steps

  1. Audit test coverage - Identify gaps between unit and E2E
  2. Add component tests - Start with critical business logic
  3. Implement TestContainers - Replace test database hacks
  4. Write contract tests - Document and verify API expectations
  5. Refactor test data - Replace fixtures with builders
  6. Parallelize tests - Ensure isolation for parallel execution
  7. Monitor test health - Track flakiness and duration

References