Hibernate & Testcontainers – A Perfect Match For Your Tests?


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


When writing tests that rely on a database, you are facing 2 challenges:

  1. You need to write meaningful tests that ensure that your application works correctly.
  2. You need to provide a test database for each test run.

I can’t help you with the 1st challenge. You know your application a lot better than I do, and I’m sure you will be able to figure this out.

But I can show you a simple technical solution to provide a test database for each test run. Using the Testcontainers library, you can easily start up a Docker container with your database for your test run.

Adding Testcontainers to Your Project

Before you can add Testcontainers to your project, you need to make sure that you have a working Docker instance running on your system. If you don’t have that or are not familiar with Docker, please take a look at https://www.docker.com/. They provide installers for various OS and host great documentation, including a getting started guide.

Adding the Testcontainers library itself to your project is simple. You only need to add a dependency to a database-specific Testcontainers module to your application. In the example of this post, I want to test my code against a PostgreSQL database. Because of that, I add a dependency to the org.testcontainers.postgresql module to my pom.xml file. There are several other modules available for other DBMS.

<dependency>
	<groupId>org.testcontainers</groupId>
	<artifactId>postgresql</artifactId>
	<version>1.14.3</version>
	<scope>test</scope>
</dependency>

Project Setup

After you’ve added the required dependency to your project, you can tell Testcontainers to start up a docker container with your test database as part of your test case. I will show you 2 options for that in this article. But before we take a look at them, we need to talk about database ports.

Most applications and test configurations expect that the database runs at a pre-defined hostname and port. A typical example is the port 5432 of a PostgreSQL database. But you can’t do that if you’re using Testcontainers. Every time it starts a new docker container with your database, it maps the container-internal port 5432 to a random port on your system.

When you use Testcontainers for the first time, exposing random ports might seem like a massive annoyance or maybe even a bug. But it isn’t. It’s a feature that makes it much easier to test multiple applications in parallel. Because Testcontainers always picks a new port, each test suite will start its own database container on a separate port without affecting any other test that runs in parallel.

Working With Random Ports

As good as using random ports might be for running tests in parallel, it creates a new challenge for your test setup. Instead of starting your application and connecting it to a predefined database host, you now need to adjust the configuration. There are 2 easy ways you can do that using plain Hibernate.

Option 1: Testcontainers JDBC Driver and a Special JDBC URL

The easiest way to automatically connect to the database container started by Testcontainers is to use their JDBC driver. You can do that by changing the javax.persistence.jdbc.driver property in your persistence.xml or your connection pool configuration to org.testcontainers.jdbc.ContainerDatabaseDriver. After you’ve done that, you can provide the specification name of the database container that Testcontainers shall start in the javax.persistence.jdbc.url configuration property.

I use this approach in the following configuration to start a PostgreSQL database server in version 13. On this server, Testcontainers shall create the recipes database.

<persistence>
    <persistence-unit name="my-persistence-unit">
		...
		
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.testcontainers.jdbc.ContainerDatabaseDriver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:tc:postgresql:13:///recipes" />
			<property name="javax.persistence.jdbc.user" value="postgres" />
			<property name="javax.persistence.jdbc.password" value="postgres" />
			
			<!-- Create database schema and add data -->
			<!-- DON'T use this in production! -->
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
            <property name="javax.persistence.sql-load-script-source" value="data.sql"/>
        </properties>
    </persistence-unit>
</persistence>

Using this configuration, Testcontainers will create an empty database, map it to a random port on your system, and connect to it via JDBC. Because you’re using Testcontainers ContainerDatabaseDriver JDBC driver, you will automatically connect to the database container.

You will also need to create the table model and maybe add some data to it. There are several ways to do that. Tools like Flyway and Liquibase provide the most powerful approaches, and I explained them in great detail in previous articles. In this example, I keep it simple and tell Hibernate to create the required database tables and use the statements in the data.sql file to load an initial set of data.

Option 2: Using environment variables in JDBC URL

If you don’t want to replace your JDBC driver with the one provided by the Testcontainers project, you can reference a system property as your database port in the configuration. In the following example, I replaced the port of the database with the property db.port.

<persistence>
    <persistence-unit name="my-persistence-unit">
		...
		
        <properties>
			<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://localhost:${db.port}/recipes" />
			<property name="javax.persistence.jdbc.user" value="postgres" />
			<property name="javax.persistence.jdbc.password" value="postgres" />
			
			<!-- Create database schema and add data -->
			<!-- DON'T use this in production! -->
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
            <property name="javax.persistence.sql-load-script-source" value="data.sql"/>
        </properties>
    </persistence-unit>
</persistence>

In the 2nd step, you need to start the database container and set the system property before you instantiate your EntityManagerFactory.

Testcontainers provides a Java API for their supported containers. In this article’s examples, I use the PostgreSQLContainer class to start a container with a PostgreSQL database server. If you want to use a specific docker container definition, you can provide its name to the constructor of the PostgreSQLContainer class. After you’ve done that, you can use an API to configure your database container.

In the following example, I tell Testcontainers to start a postgres:13 container, create the recipes database, and create the user postgres with the password postgres. In the next step, I get the port from my postgreSQLContainer object and set it as the db.port system property.

public class TestApplication {

    private EntityManagerFactory emf;

    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer<>("postgres:13")                                                                           
                                                                            .withDatabaseName("recipes")
                                                                            .withUsername("postgres")
                                                                            .withPassword("postgres");

    @Before
    public void init() {
        System.setProperty("db.port", postgreSQLContainer.getFirstMappedPort().toString());
        emf = Persistence.createEntityManagerFactory("my-persistence-unit");
    }
	
    ...
}

As you can see, this approach requires a little more work than using Testcontainers JDBC driver. Because of that, I prefer using the JDBC driver instead of starting the container programmatically.

Tips to Speed-up Your Tests

Starting a new database container for your test case and stopping it afterward makes your tests independent of their environment. But it slows down your test execution and makes it more difficult to analyze unexpected results.

Use TempFS For Faster Storage

If you’re running your tests on a Linux system, you can benefit from its temporary file storage feature, called TempFS. It’s a mounted drive that’s mapped to your memory instead of your hard drive. It provides much better performance, but you also lose your data when the container gets stopped. Docker supports that for its containers on Linux systems.

You can tell Testcontainers to start the database container using TempFS by adding the parameter TC_TMPFS to the JDBC URL.

<persistence>
    <persistence-unit name="my-persistence-unit">
		...
		
        <properties>
			<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:tc:postgresql:13:///recipes?TC_TMPFS=/testtmpfs:rw" />
			<property name="javax.persistence.jdbc.user" value="postgres" />
			<property name="javax.persistence.jdbc.password" value="postgres" />
			
			<!-- Create database schema and add data -->
			<!-- DON'T use this in production! -->
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
            <property name="javax.persistence.sql-load-script-source" value="data.sql"/>
        </properties>
    </persistence-unit>
</persistence>

Run DB in Deamon Mode

If you want to keep your database container up and running after the test got completed, you need to start it in daemon mode. This can be useful if you want to check how your test case changed the data in your database or if you need to analyze unexpected test results.

To start the container in daemon mode, you need to set the TC_DAEMON parameter in your JDBC URL to true.

<persistence>
    <persistence-unit name="my-persistence-unit">
		...
		
        <properties>
			<property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:tc:postgresql:13:///recipes?TC_DAEMON=true" />
			<property name="javax.persistence.jdbc.user" value="postgres" />
			<property name="javax.persistence.jdbc.password" value="postgres" />
			
			<!-- Create database schema and add data -->
			<!-- DON'T use this in production! -->
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
            <property name="javax.persistence.sql-load-script-source" value="data.sql"/>
        </properties>
    </persistence-unit>
</persistence>

Conclusion

Lots of developers use Docker containers to start up a database for a test run. This often requires external scripts, which you programmatically integrate into your test pipeline.

The Testcontainers library makes that much easier. You can either configure and start your container programmatically or via Testcontainers’ JDBC driver. I recommend using Testcontainers’ JDBC driver. It enables you to define the container via the JDBC URL and automatically connects your application to it.

In both cases, the database container gets automatically started before your test gets executed and shut down after the test is completed.