Skip to content

Integration Test Patterns

Your unit tests mock the database and pass with flying colors. Then you deploy and discover that a Drizzle ORM migration changed a column type from varchar(255) to text, which caused your unique constraint to silently stop working. The mock never knew. Integration tests exist to catch the things that happen when real components talk to each other — the exact place where bugs hide.

  • Patterns for database integration tests with proper setup, teardown, and isolation
  • API contract testing workflows that catch breaking changes across services
  • Strategies for testing message queues, caches, and external service integrations
  • AI prompts that generate complete test infrastructure, not just test cases
  • Techniques for keeping integration tests fast enough to run in CI

Integration tests need a real database, but not your production one. Set up an isolated test database that resets between tests.

@src/lib/db/schema.ts @tests/setup.ts
Create a database integration test setup for our Drizzle ORM + PostgreSQL project:
1. A setup file that:
- Creates a test database (or uses a container via testcontainers)
- Runs all migrations before the test suite
- Provides a clean transaction wrapper per test (rollback after each)
- Exports a `getTestDb()` helper that returns an isolated database connection
2. An example test for UserRepository.create that:
- Uses the real database (not mocks)
- Verifies the record exists after creation
- Checks unique constraint enforcement (duplicate email)
- Verifies cascade deletes work correctly
- Tests NULL handling for optional fields
Follow our existing test patterns in @tests/setup.ts

When your service consumes another service’s API, contract tests verify the integration without running both services.

Testing event-driven integrations requires verifying message publication, consumption, and ordering.

For external APIs you do not control (Stripe, Twilio, SendGrid), use stub servers that simulate the API behavior.

Create a stub server for the Stripe API that we can use in integration tests.
The stub should handle:
1. POST /v1/charges - Return success with a charge ID
2. POST /v1/charges - Return 402 when amount > 999999 (simulates declined)
3. POST /v1/refunds - Return success with refund ID
4. GET /v1/charges/{id} - Return charge details for known IDs, 404 for unknown
Implementation:
- Use Express with a random port (for parallel test execution)
- Store created charges in memory (so GET returns what POST created)
- Include a reset() method to clear state between tests
- Export a factory: createStripeStub() returns { url, server, reset }
Save to /tests/stubs/stripe.stub.ts
  1. Parallelize with isolation

    Run independent test suites in parallel. Each suite gets its own database schema or container.

  2. Shared test containers

    Start the database container once for the entire suite, not per test file. Use transaction rollback for per-test isolation.

  3. Selective running

    Tag tests and run only affected integration tests: npm test -- --testPathPattern=integration/order when working on order code.

  4. Database seeding vs. per-test setup

    For read-only tests, seed reference data once. Only create per-test data for tests that modify it.

  5. Container caching in CI

    Cache Docker images in your CI pipeline to avoid pulling them on every run.

“Integration tests are flaky and fail randomly in CI.” The top causes: shared database state between tests, hardcoded ports conflicting in parallel runs, and timing-dependent assertions. Use transaction rollback for isolation, random ports for services, and polling loops instead of setTimeout for async operations.

“Tests pass locally but fail in CI.” Check for differences in database versions, missing environment variables, and Docker networking. Use testcontainers to ensure the same database version runs everywhere. Pin your container images to specific versions.

“Integration tests are too slow to run on every commit.” Run only affected integration tests on commits (based on changed files). Run the full suite on PR merge and nightly. This gives fast feedback during development and comprehensive verification before merge.

“The AI generated tests that depend on insertion order.” Database query results are unordered unless you specify ORDER BY. Ask the AI to “sort results before asserting, or assert on set membership rather than array equality.”