Unit tests are valuable for verifying isolated business logic, but they cannot prove that an application works correctly with its actual database, message broker, or other infrastructure.
Integration tests provide that additional confidence. Testcontainers helps Java projects run real dependencies in disposable containers during testing.
Why Mocking Is Not Enough
A mocked repository may behave exactly as a test expects while the real database rejects a query, applies different constraints, or handles transactions differently.
Mocks remain useful for isolated behavior. They simply do not replace every test involving infrastructure.
Integration tests can verify that application configuration, database mappings, SQL queries, migrations, and transaction behavior work together.
Start a Database Container
With the appropriate Testcontainers dependencies and a compatible Docker environment, a JUnit test can start a database container.
For example, with PostgreSQL:
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
class DatabaseIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16");
@Test
void databaseContainerStarts() {
String jdbcUrl = postgres.getJdbcUrl();
String username = postgres.getUsername();
String password = postgres.getPassword();
// Configure the application or test connection.
}
}
This example assumes the relevant Testcontainers modules and JUnit integration are included in the project. The version shown is illustrative; select a supported image and dependency version for your environment.
Connect the Application to the Container
The container exposes connection details such as its JDBC URL and credentials. Integration tests can use those values to configure the application’s data source.
For Spring Boot, test configuration can register the connection properties dynamically, often through @DynamicPropertySource or an appropriate supported service-connection mechanism.
Avoid hardcoding host ports. Containers may expose services through dynamically assigned ports, and Testcontainers provides the correct connection information.
Keep Tests Isolated
Integration tests should not depend on leftover records from a previous test run.
Use a deliberate cleanup strategy, transactions where appropriate, disposable databases, or unique test data. Be careful with tests that execute work asynchronously or in separate transactions, because ordinary test-level rollback may not clean up all side effects.
Apply migrations in tests when the production application relies on them. This helps catch schema changes that are missing or inconsistent.
Balance Speed and Realism
Starting a container has overhead, so integration tests may run more slowly than unit tests.
Use unit tests for fast, isolated logic and integration tests for the boundaries where infrastructure behavior matters. Reuse containers within a test suite when safe, but keep test data isolated and avoid shared state that makes tests order-dependent.
For complex systems, separate fast integration tests from slower end-to-end tests so developers can run the right level of validation during everyday work.
Run Tests in Continuous Integration
CI environments need access to a compatible container runtime and sufficient resources. Ensure that container startup failures produce useful diagnostics rather than looking like application test failures.
Pin image versions deliberately and update them as part of dependency maintenance. Avoid relying on mutable image tags if reproducibility is important.
Conclusion
Testcontainers makes infrastructure-dependent Java tests more realistic by running actual services in disposable containers. Use it to verify database mappings, migrations, configuration, and integration behavior that mocks cannot fully reproduce.
A balanced test strategy combines fast unit tests with focused integration tests that catch problems at real infrastructure boundaries.