Need the #1 custom application developer in Brisbane?Click here →

Testing

Integration Testing

9 min readLast reviewed: March 2026

Integration tests verify that multiple components work correctly together. While unit tests test functions in isolation, integration tests test real interactions: API endpoints hitting a database, components calling APIs, or services working with third-party tools. These tests catch bugs that unit tests miss because they test across boundaries.

What Integration Tests Verify

Integration tests go beyond a single function. They test:

  • API endpoints: A request comes in, the endpoint validates it, queries the database, and returns the right response with correct status codes.
  • Database interactions: Does the code correctly insert, update, query, and delete data? Does it handle transactions properly?
  • Component interactions: Does a component correctly call an API and display the results? Does it handle loading and error states?
  • Service-to-service communication: If your app calls Stripe, Twilio, or another service, does it handle the response correctly? Does it fail gracefully if the service is down?
  • Data flow: Data enters through the API, gets processed, stored, and returned correctly through the full stack.

The key difference: integration tests use real components but often with mocked external services. You use a real database (or a test copy), real application logic, but you mock third-party APIs.

Testing API Endpoints

A common integration test pattern is testing an API endpoint completely. For a Node.js API, tools like Supertest make this straightforward. You make an HTTP request to the endpoint, then assert on the response status, headers, and body.

A typical test might:

  1. Insert test data into the database (arrange)
  2. Make an HTTP request to the endpoint (act)
  3. Assert the response status is 200, the body contains the expected data, and the database state is correct (assert)

These tests are slower than unit tests (they hit a real database) but faster than E2E tests (they don't use a browser). They catch bugs in input validation, database queries, and response formatting.

Test Databases and Data Seeding

Integration tests need data to work with. You don't want tests touching the production database, and you don't want tests interfering with each other. The solution: each test suite runs against a fresh test database.

Common patterns:

  • Create a test database: Use a separate database (PostgreSQL test DB, SQLite in-memory, etc.) that's wiped and reset before each test.
  • Run migrations: Before tests run, apply your database migrations to the test DB so it mirrors production schema.
  • Seed data: Insert test data (fixtures or factories) that your tests depend on.
  • Clean up: After each test, delete the data it created so the next test starts fresh.

Many teams use fixtures (predefined test data) or factories (functions that generate test data). For example, a userFactory function that creates a test user with default values, overridable by the test. This keeps test data management DRY.

Tip
Test database isolation: If tests can run in parallel, each test thread needs its own database connection or transaction. Otherwise, one test's data cleanup can interfere with another test's assertions. Using transactions that rollback after each test is a clean approach.

Mocking Third-Party Integrations

When your code calls an external service (Stripe for payments, Twilio for SMS, SendGrid for email), you have two options:

  • Mock it: Replace the service with a fake that returns predefined responses. Fast, no cost, fully controlled.
  • Use a sandbox: Call the real service's test environment. More realistic but slower and sometimes requires credentials.

For most integration tests, mocking is the right choice. You test that your code calls the service correctly (with the right arguments) and handles the response properly. You don't need to test that Stripe's code works—they do that. A separate test (maybe just one integration test) can verify the real integration in a sandbox if needed.

Many services provide SDKs with built-in mocking or testing utilities. Stripe provides testing card numbers; AWS provides LocalStack; Firebase provides emulators. Use these when available.

Service-Level vs Component-Level Integration Tests

Integration tests can be organized at different levels:

  • Service-level (API endpoint tests): Test complete API endpoints. Request comes in, response comes out. Tests the full backend stack.
  • Component-level (frontend integration): Test React components that fetch data from an API. Use a mock server (MSW—Mock Service Worker) so tests don't depend on a running backend.
  • Library integration: Test that your code correctly uses a third-party library (database driver, HTTP client, etc.).

Frontend integration tests are increasingly popular. Mock the API with Mock Service Worker (MSW), then test that the component makes the right requests and displays the results correctly. This is faster and more reliable than E2E tests but more realistic than unit tests.

When Integration Tests Catch Bugs Unit Tests Miss

Unit tests can pass while integration tests fail. Common scenarios:

  • Database schema mismatches: Your code assumes a column exists, but the migration hasn't been applied.
  • SQL injection or query errors: Raw SQL queries work in isolation but fail with real data.
  • Race conditions: Concurrent requests cause issues. Unit tests don't test concurrency.
  • API contract violations: Your code doesn't send the data shape the frontend expects.
  • Missing validation: Unit tests might mock a validator; integration tests use the real one.
  • Dependency version mismatches: Library A works in isolation but conflicts with library B.

This is why the testing pyramid includes integration tests. They catch real-world integration bugs that unit tests can't see.

Note
The sweet spot: Use unit tests for logic, integration tests for API endpoints and data flow. This combination gives you good coverage with reasonable speed. Use E2E tests only for critical user journeys. Aim for 70% unit, 20% integration, 10% E2E.

Database Cleanup Between Tests

A critical rule: tests must be independent. One test's data shouldn't affect another test. Cleanup strategies:

  • Truncate tables: After each test, delete all data from the tables. Simple but slow for large databases.
  • Transactions: Wrap each test in a transaction that rolls back after completion. Data changes are discarded. Fast and clean.
  • Test-specific databases: Each test gets its own database copy. Slowest to set up but most isolated.
  • Factories with unique data: Each test creates data with unique identifiers so they don't collide.

Most teams use transactions for speed. For very large databases where truncate is too slow, consider a shared clean database with ID namespacing.

Continuous Integration and Test Speed

Integration tests are slower than unit tests, but still need to be fast. A test suite taking 10 minutes in CI gives slow feedback. Slow CI means developers wait and lose context. Strategies for keeping integration tests fast:

  • Parallel execution: Run tests in parallel on multiple CI workers. With 4 workers, a 10-minute test suite becomes 2.5 minutes.
  • Database pooling: Reuse database connections instead of creating a new one for each test.
  • Selective testing: Only run integration tests that could be affected by the change. Use code analysis to determine impact.
  • Mock expensive operations: If tests make real API calls, mock them. Only test the real integration occasionally.
  • Use fast databases: SQLite in-memory is faster than PostgreSQL. Reasonable for tests; still realistic enough.

Balance: integration tests catch important bugs, but they shouldn't slow down development. Find the sweet spot for your team and project.

Fixtures and Factories

Test data management matters. Two common patterns:

AspectFixturesFactories
DefinitionStatic test data files (JSON, SQL) loaded before testsFunctions that generate test data dynamically
FlexibilityLess flexible; all tests share the same dataMore flexible; each test creates custom data
Setup timeFast; just load the fileSlightly slower; generates data per test
MaintenanceCan become stale; hard to update schemaEasier to maintain; code is versioned
ReadabilityVisible test data (easy to see what data a test uses)Logic-based (harder to know exact data)
IsolationTests might interfere if they modify dataBetter isolation; each test has unique data

Most modern teams use factories. They're more maintainable and flexible. Libraries like Faker generate realistic test data (fake names, emails, dates) making tests more realistic.

Common Integration Testing Mistakes

Avoid these pitfalls:

  • Testing with production data: Never. Always use test databases with non-sensitive data.
  • Tests that depend on each other: Test A inserts data that Test B relies on. If A fails, B mysteriously fails. Each test must be independent.
  • Not cleaning up: Test A modifies the database and doesn't clean up. Test B expects clean state and fails randomly.
  • Testing external services without mocking: Your tests fail when Stripe is down. Mock it and focus on your code.
  • Too many integration tests: Integration tests are slower. If every test is integration, your suite runs for hours. Focus on API endpoints; test business logic with units.
  • Flaky database tests: Tests fail randomly due to race conditions or timing. Usually a sign tests aren't isolated properly.
Developer Insight
Good integration test: Tests an API endpoint completely. Inserts test data, makes the request, verifies the response, checks the database was updated correctly, and cleans up. Takes less than a second. Passes consistently. If it fails, you know which component is broken.

Getting Started with Integration Tests

If you're adding integration tests to an existing project:

  1. Set up a test database (separate from production).
  2. Choose a testing tool (Supertest for Node.js HTTP endpoints, Pytest for Python, etc.).
  3. Write tests for your most critical API endpoints first (login, payment, data creation).
  4. Use factories to manage test data.
  5. Ensure tests run in parallel and clean up after themselves.
  6. Add integration tests to your CI pipeline (usually after unit tests pass).

Key Takeaways

Integration tests verify that multiple components work together. They test API endpoints, database interactions, and service integrations. They catch bugs that unit tests miss but are slower than unit tests. Use a test database, factories for data, and mock external services. Keep tests independent and fast. Balance integration tests with unit tests: write many unit tests for logic, focused integration tests for critical paths, and a few E2E tests for end-to-end user journeys.