Testcontainers lets database integration tests run against a real PostgreSQL, MySQL, or other supported database engine in a disposable container. That gives tests database-specific SQL behavior and features that an in-memory substitute such as H2 may not reproduce, while keeping test data separate from a developer’s shared database. The trade-off is extra startup and runtime cost, so use it for tests that need to verify persistence behavior rather than for every unit test.
What Testcontainers changes about a database test
Instead of pointing a test at an in-memory database or a long-lived developer database, Testcontainers starts the database engine your test needs, waits for it to become usable, and makes its connection details available to the test. The database runs in a container, not inside the application process.
The Testcontainers for Java documentation describes this as “100% database compatibility” because the container runs a real database. Treat that as a qualitative compatibility claim, not as an independent benchmark or a guarantee that every environment detail matches production. Your container image, database version, extensions, configuration, schema, and test data still need to reflect the behavior you intend to check.
Isolation depends on how you scope and clean up the container and its data. A dedicated container or otherwise isolated database state avoids contamination from a developer’s machine or another test run; tests sharing one container must still prevent their own fixtures from interfering with one another.
#1 Best Overall
Choose Testcontainers, H2, or a shared database
| Approach | Database behavior | Isolation and repeatability | Runtime and operational trade-off | Best fit |
|---|---|---|---|---|
| Testcontainers | Runs a real database engine, so engine-specific SQL and features are closer to production. | Can provide an isolated database state instead of depending on a developer’s machine or a shared service. Test data still needs deliberate management when tests share a container. | Requires a supported Docker-API-compatible runtime and takes more startup/runtime than H2. Performance depends on the project’s images, schema, host, and CI environment. | Focused integration tests for migrations, queries, constraints, transactions, and persistence behavior tied to the production database. |
| H2 or another in-memory substitute | Fast, but does not reproduce every behavior of a different production database. | In-memory state can be convenient to reset; results remain limited by differences between engines. | The Testcontainers Java documentation says Testcontainers is not as performant as H2. | Fast tests for logic that does not depend on production-engine behavior, when the substitute’s limitations are acceptable. |
| Shared developer or test database | May use the desired engine, depending on how it is provisioned. | Other users or runs can alter state, making failures harder to reproduce unless databases, schemas, or fixtures are isolated. | Requires managing a persistent service and its credentials, lifecycle, and cleanup. | Only when a shared service is intentional and the project can reliably isolate and reset test data. |
Do not migrate every test to a container just to claim that the suite uses the production engine. The database-module documentation recommends keeping the number of database-hitting tests as small as practical and using mocks for higher-level components. A useful split is fast unit tests for business rules, a focused set of database integration tests for persistence, and a smaller number of end-to-end tests for application behavior across components.
Set up Java tests with a Testcontainers JDBC URL
For a Java application that connects through JDBC, URL mode is the shortest path. Add Testcontainers and the module for the database you intend to run to the test dependencies, along with the database’s JDBC driver. Use dependency versions compatible with the project and database; no version is prescribed here.
- Start from the application’s JDBC URL. Insert
tc:afterjdbc:. The Testcontainers documentation givesjdbc:tc:postgresql:9.6.8:///databasenameas an example. That version is illustrative, not a recommendation: select an image version aligned with the database behavior you need to test. - Configure the test application to use that URL. Testcontainers starts the database container when the connection is requested. In URL mode, the host and port in the URL are ignored by Testcontainers; do not use them to force a fixed host port.
- Initialize only what the test needs. A classpath SQL script can run before the application receives a connection. For example, the documentation shows
TC_INITSCRIPT=somepath/init_mysql.sql. Alternatively, let the application’s normal migration process build the schema against the started database. - Run the test through the normal application connection path. Assertions should exercise the repositories, queries, or persistence behavior that matters, rather than merely checking that a container started.
The Java documentation lists JDBC URL forms for PostgreSQL, MySQL, MariaDB, SQL Server, Oracle, DB2, CockroachDB, ClickHouse, PostGIS, TimescaleDB, PGVector, TiDB, Trino, YugabyteDB, and other supported databases. Check the module documentation for the exact URL form and image support for the database you use.
Use an explicit database container when URL mode is not enough
An explicit typed container is useful when a test needs direct control of container lifecycle or configuration, or when the application’s connection settings are assembled outside a JDBC URL. Start the database container before the application-under-test needs its connection, then pass the values exposed by the container into the application configuration.
String jdbcUrl = database.getJdbcUrl();
String username = database.getUsername();
String password = database.getPassword();
Here, database represents the typed container object created for the chosen engine. Use the URL, username, and password it reports rather than assuming a fixed port or credentials. This lets the test configure the application against the actual container instance.
Wait for readiness and avoid port collisions
A running container process is not necessarily a database ready to accept the application’s connection. Testcontainers starts required services before test execution, waits for usability, and exposes connection details for the test. Database modules include relevant wait strategies; services with unusual startup behavior can use a custom or composite strategy.
The Java startup-and-waits documentation says the ordinary default wait is up to 60 seconds for the first mapped network port to listen. A listening port is a startup check, not proof that every application-level setup step, such as migrations or fixture loading, has completed. If tests race database initialization, use an appropriate readiness check and ensure setup completes before the application or assertions proceed.
Testcontainers maps container ports to host ports dynamically. This avoids needing each local run or parallel CI job to claim the same fixed host port. Use the connection details provided by Testcontainers rather than hard-coding a host port such as 5432.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Keep test data isolated and migrations realistic
Choose the isolation boundary to match the test. A container may be shared for a test class or suite to avoid repeated startup, but state then persists between tests in that container. Ensure tests establish the data they need and remove or reset state that could affect later tests. For tests that must be independent, use a fresh or otherwise isolated database state.
Rank #4
- Test SQL and constraints: seed only the rows needed to verify the query, uniqueness rule, foreign key, transaction, or other behavior under test.
- Test migrations: run the same migration mechanism used by the application against the container when migration compatibility is part of the risk.
- Test repeatability: avoid relying on rows, schemas, or local services left behind by a prior run.
- Keep scope focused: use mocks or unit tests where database semantics are irrelevant, and reserve container-backed tests for persistence behavior.
An initialization script is useful for a controlled starting schema or fixture. Application migrations are a better fit when the test needs to establish that the project’s own migration path works against the selected database engine.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Make sure a compatible container runtime is available
Testcontainers needs a Docker-API-compatible runtime. Its getting-started documentation identifies Docker Desktop, Docker Engine on Linux, and Testcontainers Cloud as supported runtime options. The same requirement applies whether tests are launched from an IDE or CI: the selected runtime must be available to the test process.
Testcontainers has implementations for Java as well as Go, .NET, Node.js, Python, Rust, Ruby, PHP, Haskell, Clojure, Elixir, Scala, and Native. The Java JDBC URL and typed-container patterns above are Java-specific; use the documentation for the implementation and database integration in your language.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Best Value
Reactive applications need the R2DBC integration
For a reactive application using R2DBC rather than JDBC, use Testcontainers’ R2DBC integration instead of applying the JDBC URL pattern unchanged. The R2DBC documentation requires the TC_IMAGE_TAG parameter to identify the database image tag. Configure the application with the R2DBC connection details and tag format documented for the integration you use.
Should you reuse containers?
Reusable containers can retain a matching container between executions, but the Java documentation labels this capability experimental, warns that it may not support all features, and explicitly says it is not suited for CI. Treat it as a local-development optimization only: opt in using the documented environment or user-property setting, measure the effect on your suite, and deliberately manage data so a retained database does not make tests depend on earlier runs.
Do not use container reuse as a substitute for sound test isolation. A persistent container can reduce repeated setup but also retain state; tests must still create and clean the data they rely on.
Measure the cost in your own test environment
Testcontainers has a startup and runtime cost compared with H2, but there is no general benchmark that predicts the impact for every project. The result depends on the database image, schema and migration work, host, runtime, and CI environment. Time the suite in the environments that matter, then keep container-backed tests concentrated on behavior where real-engine fidelity changes confidence in the result.
Recommended Free Tools
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

