Integration Testing: Strategies and Patterns
Practical strategies for integration testing in modern applications. How to test API endpoints, database interactions, external services, and multi-component workflows.
Strategic Systems Architect & Enterprise Software Developer
Why Unit Tests Alone Are Insufficient
Unit tests verify that individual functions produce correct output given specific input. They're fast, isolated, and valuable. But they have a fundamental blind spot: they can't tell you whether those functions work together correctly.
A unit test confirms that your validation function rejects invalid email addresses. Another unit test confirms that your user service calls the database correctly. A third confirms that your API controller returns the right status codes. Each test passes. But when a real request hits your API, the controller doesn't call the validation function before passing data to the service, and invalid email addresses reach the database. Every unit passed. The application is broken.
Integration tests fill this gap by testing the interactions between components. They verify that modules connect correctly, that data flows through the system as expected, and that the boundaries between components — the places where most bugs actually live — behave properly.
The challenge is that integration tests are harder to write, slower to run, and more fragile than unit tests. Getting value from integration testing requires deliberate strategy about what to test, how to set up the test environment, and where the investment produces the best return.
What to Integration Test
The most valuable integration tests cover three areas.
API endpoint tests send real HTTP requests to your application and verify the complete response — status code, headers, response body, and side effects. These tests exercise the full request lifecycle: middleware, routing, validation, business logic, database interaction, and serialization. A single API test often covers more meaningful behavior than a dozen unit tests because it verifies the integration points where bugs actually occur.
For a Nuxt or Express application, this means starting the application server, sending requests with a test HTTP client, and asserting on the responses. Use a test database that gets seeded before each test suite and cleaned up after. The setup is more involved than unit testing, but the confidence these tests provide is proportionally higher.
Database interaction tests verify that your queries, migrations, and data access layer work correctly against a real database — not a mock. ORM-generated queries, complex joins, transactions with rollback behavior, and constraint violations are all common sources of bugs that only surface when running against a real database engine. If you're using Prisma or a similar ORM, test the generated queries against a real database instance to catch issues with schema mismatches and query optimization.
External service integration tests verify that your application correctly communicates with third-party APIs, message queues, and other external systems. These are the most complex integration tests because they involve systems you don't control. Use a combination of approaches: contract tests that verify your expectations about the external API, recorded response fixtures for deterministic testing, and periodic live integration tests that run against sandbox or staging environments.
Setting Up the Test Environment
Integration test reliability depends heavily on environment setup. Tests that share state — a database that isn't cleaned between tests, a server that isn't restarted — produce intermittent failures that erode trust in the test suite.
Database isolation is the most critical factor. Each test or test suite should start with a known database state and leave no residue when it finishes. Three common approaches: transaction wrapping (start a transaction before each test and roll it back after), truncation (delete all data from tables between tests), and fresh database (create a new database for each test run). Transaction wrapping is fastest but doesn't test transaction behavior. Truncation is a good middle ground. Fresh databases are safest but slowest.
Test containers simplify running real databases and services in test environments. Docker containers for PostgreSQL, Redis, and other dependencies can be started before the test suite and destroyed after. This eliminates the "works locally, fails in CI" problem because both environments use identical service versions and configurations.
Fixture management deserves more attention than it usually gets. Tests that build their own data setup — creating users, orders, and relationships before each test — are verbose and fragile. Build a factory or seed system that creates realistic test data with sensible defaults and easy overrides. Well-designed fixtures make tests shorter, more readable, and more maintainable.
Patterns for Reliable Integration Tests
Test the happy path and the important error paths. Integration tests are expensive, so be selective. Every API endpoint deserves a happy-path integration test. Error paths should be tested at the integration level only when the error handling involves multiple components — for example, verifying that a payment failure rolls back the order and notifies the user. Simple validation errors are better covered by unit tests.
Keep integration tests deterministic. Avoid tests that depend on wall-clock time, random values, or external service availability. Mock external services at the HTTP level using tools like MSW (Mock Service Worker) for controlled, deterministic responses. Use fixed dates and UUIDs in tests rather than generating them dynamically.
Organize tests by feature, not by layer. A test file for the "checkout" feature that covers the API endpoint, the database interactions, and the payment service integration provides more useful feedback than separate files for "controller tests," "service tests," and "repository tests." When the checkout test fails, you know immediately which feature is broken and can investigate the full stack in one place.
Run integration tests in CI on every pull request. Integration tests that run only in nightly builds catch bugs too late. Modern CI services handle Docker-based test environments efficiently, and a five-minute integration test suite that runs on every PR is worth more than a comprehensive suite that runs once a day.
The testing pyramid — many unit tests, fewer integration tests, even fewer end-to-end tests — remains valid as a general guide. But the pyramid's proportions should reflect your application's risk profile. An application with complex business logic and simple integrations should lean toward unit tests. An application that orchestrates many services with simple individual logic should lean toward integration tests. Let the architecture inform the testing strategy, not the other way around.