← All posts
6 min read

Integration Tests: Verifying Real Boundaries

#technology#integration-tests#software-testing#quality
📑 On this page

Two components can pass every isolated test and still fail when connected.

One expects a timestamp in seconds, another sends milliseconds. One database field allows null while the application assumes a value. One service returns user_id while its client reads userId.

Integration tests provide evidence that real components agree at their boundaries.

They trade some speed and isolation for realism where mismatched assumptions are likely.

A concrete example: an order repository

An application repository writes an order and its items to a relational database.

An integration test can:

  1. start a real compatible database,
  2. apply actual migrations,
  3. write an order through the repository,
  4. read it back,
  5. verify item relationships and money precision,
  6. test rollback on failure, and
  7. clean up the test data.

A mocked database client would not reveal an invalid SQL statement or schema mismatch.

Choosing the integration boundary

Integration does not always mean testing the entire product.

Useful boundaries include:

  • application plus database,
  • producer plus message serializer,
  • HTTP handler plus framework routing,
  • file importer plus parser,
  • service client plus a realistic stub server,
  • or several modules inside one process.

Choose the smallest boundary that includes the risk you need evidence about.

Use real components where they matter

If the purpose is to verify SQL behavior, use the real database engine or a highly compatible instance.

An in-memory substitute may differ in:

  • data types,
  • transactions,
  • constraints,
  • query syntax,
  • collation,
  • locking,
  • and concurrency.

Faster substitutes are useful only when their differences do not invalidate the question.

Containers and temporary infrastructure

Containers make it practical to start isolated databases, queues, and other services for tests.

They improve repeatability by pinning versions and configuration. They do not automatically guarantee realism; production may use different extensions, permissions, network policies, or managed-service behavior.

Test infrastructure still needs an explicit fidelity decision.

Database migrations

Integration tests should build schemas through the same migrations used by deployment.

Creating tables through a separate test-only script can hide a broken migration sequence. Testing from an empty database and testing upgrades from representative older versions answer different questions.

Migration behavior is part of the application's integration contract.

Transactions and cleanup

Tests need isolated data.

Common strategies include:

  • wrap each test in a transaction and roll it back,
  • use a unique database schema,
  • create uniquely named records,
  • reset the service between suites,
  • or provision a temporary instance.

Cleanup must also work after failure, otherwise later tests inherit confusing state.

Contract testing

When two services deploy independently, a contract test checks whether provider and consumer assumptions remain compatible.

A consumer may declare that it needs:

  • a field with a particular type,
  • a specific error response,
  • or an endpoint accepting one request shape.

Provider verification can detect breaking changes before both services meet in a shared environment.

Stubs for external services

Calling a real third-party payment or email provider on every test is slow, costly, and unreliable.

A realistic local stub server can verify:

  • HTTP methods and paths,
  • headers,
  • request serialization,
  • response parsing,
  • timeouts,
  • and error handling.

It should reflect the provider contract, and a smaller set of sandbox tests can check that the stub has not drifted.

Message queues

Queue integration tests can verify:

  • serialization,
  • routing keys,
  • acknowledgment behavior,
  • retry and dead-letter handling,
  • duplicate delivery,
  • and consumer idempotency.

A direct function call skips the broker behavior and may create false confidence about asynchronous systems.

File-system integration

File code depends on paths, permissions, encodings, atomic moves, and operating-system behavior.

Tests should use temporary directories and explicit file contents. Avoid depending on a developer's current directory or preexisting machine files.

Cross-platform applications may need tests on multiple operating systems.

Network behavior

Integration tests can expose:

  • incorrect timeouts,
  • connection reuse issues,
  • TLS configuration,
  • malformed headers,
  • and partial responses.

Local networks are much faster and more reliable than production, so fault injection may be needed to test delay and interruption intentionally.

Failure-path integration

Happy-path connections are not enough.

Test what happens when:

  • the database rejects a constraint,
  • a dependency times out,
  • a message is delivered twice,
  • a file is truncated,
  • credentials expire,
  • or one step succeeds before another fails.

These cases reveal whether boundaries preserve consistent state.

Parallel execution

Integration suites often run concurrently to reduce time.

Shared ports, database names, accounts, and queue topics can cause collisions. Give each worker isolated resources or unique namespaces and avoid global cleanup that deletes another worker's data.

Parallel safety is part of test design.

Determinism and waiting

Asynchronous integration tests should wait for observable conditions rather than sleep for guessed durations.

Instead of "sleep two seconds," poll for the expected event with a sensible timeout. Fixed sleeps make tests both slow and flaky when machines vary.

Failure output should show which condition never became true.

Seed data

Seed only the data required for the behavior.

Huge production-like snapshots slow tests, hide dependencies, and may contain sensitive information. Focused factories or fixtures make scenarios understandable.

Some performance and migration tests need larger representative datasets, but that is a deliberate separate purpose.

Test environment ownership

Integration infrastructure requires maintenance:

  • version updates,
  • startup scripts,
  • health checks,
  • cleanup,
  • credentials,
  • and diagnosis tools.

An unreliable shared environment teaches teams to ignore failures. Local or ephemeral environments often produce clearer ownership and reproducibility.

Balancing realism and speed

Not every rule needs a database or network.

Keep pure business cases in unit tests and reserve integration tests for behavior that depends on the boundary. This creates a suite where:

  • local rules fail quickly,
  • connection assumptions receive realistic checks,
  • and expensive tests remain focused.

More realism is useful only when it adds relevant evidence.

A practical integration-test checklist

Ask:

  1. Which boundary risk is under test?
  2. Is the important dependency real enough?
  3. Does setup use production-like configuration and migrations?
  4. Is data isolated across tests and workers?
  5. Are failures, retries, and partial completion covered?
  6. Are waits condition-based?
  7. Can the test run from a clean machine?

These questions keep integration tests realistic without turning them into uncontrolled mini-production systems.

Knowledge check

  1. Why can two components pass unit tests but fail together?
  2. When can an in-memory database substitute be misleading?
  3. What does a consumer-driven contract test protect?
  4. Why is condition-based waiting better than fixed sleeping?
  5. Which logic should usually remain in faster unit tests?

The one idea to remember

Integration tests verify that real components agree at meaningful boundaries. Use realistic dependencies for the risk in question, isolate data, exercise failure behavior, and keep pure local rules in faster tests.