What Integration Tests Should Actually Be Testing

by Arif Ikhsanudin, Backend Developer

The Expensive Category

Integration tests are the most expensive tests to write and maintain. They require infrastructure — databases, message brokers, external services — and they are slower to run than unit tests by an order of magnitude or more. The investment has to be justified by what the tests actually provide.

Many integration test suites waste this investment by testing things that unit tests cover better, or by not testing the things integration tests uniquely can. The result is a slow suite that does not add much value over a well-written unit suite — and a false sense of coverage.

What Integration Tests Uniquely Cover

The value of integration tests is in verifying behavior that requires real infrastructure — behavior that cannot be meaningfully tested with mocks. Specifically:

Database query correctness and performance. SQL queries that look logically correct can be semantically wrong. Queries that use the wrong join type, that produce wrong results with NULL values, that fail on edge cases in the data, or that use indexes incorrectly will only be caught by running against a real database with representative data.

-- This query looks fine but returns wrong results when customer has no orders
-- (NULL from the left join causes the sum to return NULL instead of 0)
SELECT c.id, SUM(o.total) as lifetime_value
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id

-- The integration test that catches this:
-- Insert a customer with no orders, assert lifetime_value == 0.00
-- Not 0.0, not NULL — the exact typed zero the application expects

ORM mapping fidelity. The gap between what an ORM generates and what the database actually stores is a constant source of bugs — precision loss in decimal types, timezone handling in timestamps, enum mapping failures, bidirectional relationship management. These require a real database to catch.

Message queue producer/consumer contracts. If your service publishes events to Kafka or RabbitMQ, integration tests should verify that the events are serialized correctly, that the consumer can deserialize them, and that schema evolution does not break existing consumers. This cannot be done with mocks of the broker.

HTTP client configuration and behavior. Retry logic, timeout handling, redirect following, TLS certificate validation, connection pooling — all of this requires a real (or mock) HTTP server. WireMock and MockServer are standard tools for standing up controllable HTTP servers in integration tests.

Transaction boundary behavior. Tests that verify that multiple operations within a transaction either all succeed or all roll back require a real database with real transaction support.

What Integration Tests Should Not Be Testing

Business logic that does not depend on infrastructure. If you are testing a discount calculation, a sorting algorithm, or a validation rule, use a unit test. Running these through an integration test just to exercise more code is not adding value — it is adding latency and flakiness for the same assertion.

Error handling that can be exercised with a mock. If you want to verify that your code returns a 500 when the database throws an exception, a mock that throws an exception tells you this just as well as a real database. Reserve the real database for tests where the specific behavior of the real database is what you are testing.

User-facing workflows end-to-end. A test that simulates a full user registration flow — form submission through the API through the database through the email service — is an end-to-end test. It belongs in a separate suite with different infrastructure (real SMTP or a mail catcher, full application stack). Mixing it into integration tests inflates the suite scope.

Structuring the Integration Suite

Organize integration tests by the boundary they are testing, not by the feature they relate to. A repository-layer test suite tests all database interactions. An HTTP client test suite tests all external service interactions. This structure makes it easy to see what boundaries are covered and to run only the relevant subset when changing a specific layer.

Use Testcontainers for databases rather than shared test databases. Shared test databases drift in schema, accumulate stale data, and create ordering dependencies between tests. A containerized PostgreSQL or MySQL that is provisioned fresh for each test run eliminates these issues entirely — and the startup overhead (typically 5–15 seconds for a PostgreSQL container) is amortized across the full test run.

The integration test suite is where you invest in confidence about the infrastructure layer. Spend it on the behavior that only the real infrastructure can verify.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

What Clients Often Get Wrong When Outsourcing Development

Outsourcing development seems simple: hire, delegate, and wait for results. In reality, many clients misunderstand what it takes to build quality software remotely.

Read more

How to Run Your Spring Boot App and Database Together With Docker Compose

Getting a Spring Boot application and PostgreSQL to start together correctly in Docker Compose requires more than just listing both services — you need health checks, proper dependency ordering, and connection URL configuration that works inside a container network.

Read more

Scope Creep Is Not the Client's Fault. It Is a Communication Problem.

Scope creep does not happen because clients are difficult. It happens because the original scope was never clearly enough defined — and that is usually the contractor's responsibility.

Read more

Lazy vs Eager Loading in JPA — What Gets Loaded and When

JPA's fetch type determines when associated data is loaded from the database. Getting it wrong in either direction — too eager or too lazy — produces either unnecessary data transfer or N+1 queries. Here is the model and the correct defaults.

Read more