Testing JPA with TestContainers

Testcontainers is an open-source Java library that simplifies integration testing by providing lightweight, disposable containers for database systems, message brokers, and other third-party services. In this article we will learn how to combine Testcontainer with a JPA/Hibernate Test case.

TestContainer and Databases

Firstly, if you are new to TestContainers, we recommend checking this article for a quick overview of it: Getting Started with Testcontainers for Java

Testcontainers enables you to test against real instances of databases (MySQL, PostgreSQL, MongoDB, etc.) or other services (Kafka, Redis, Elasticsearch) rather than using in-memory or mocked versions. This ensures more accurate and realistic testing scenarios.

In order to test an application with TestContainer, make sure you have the Docker service up and running:

service docker start

Then, let’s see which are the key dependencies you need to add in a TestContainer application which run Tests against a database:

<!-- MySQL JDBC Driver -->
<dependency>
          <groupId>com.mysql</groupId>
          <artifactId>mysql-connector-j</artifactId>
          <version>8.1.0</version>
          <scope>runtime</scope>
</dependency>
<!-- TestContainer core API -->
<dependency>
          <groupId>org.testcontainers</groupId>
          <artifactId>testcontainers</artifactId>
          <scope>test</scope>
</dependency>
<!-- MySQL TestContainer -->
<dependency>
          <groupId>org.testcontainers</groupId>
          <artifactId>mysql</artifactId>
          <scope>test</scope>
</dependency>

As you can see, the key dependencies are:

  • The JDBC Driver which you need for the JPA
  • The core TestContainer dependency
  • Finally, the TestContainer dependency for the Database you are going to Test

Coding the Test Class

Then, let’s code the real TestContainer Test Class:

@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestContainerJPA {

  private EntityManagerFactory emf;

  @Container
  private static final MySQLContainer MYSQL_CONTAINER = (MySQLContainer) new MySQLContainer()
          .withDatabaseName("testdb")
          .withUsername("root")
          .withPassword("password")
          .withReuse(true);

  @BeforeAll
  void setup() {
    System.setProperty("db.port", MYSQL_CONTAINER.getFirstMappedPort().toString());
    emf = Persistence.createEntityManagerFactory("my-persistence");
  }

  @Test
  void contextLoads() {
    EntityManager entityManager = emf.createEntityManager();
    entityManager.getTransaction().begin();
    Customer c = new Customer();
    c.setFirstName("John");
    c.setLastName("Doe");
    c.setEmail("[email protected]");
    entityManager.persist(c);

    entityManager.getTransaction().commit();

    List<Customer> customers = entityManager.createQuery("SELECT c FROM Customer c", Customer.class)
            .getResultList();

    for (Customer customer : customers) {
      System.out.println(customers);
    }
    Assertions.assertEquals(1, customers.size());
  }
}
  • The @Testcontainers annotation allows us to integrate Testcontainers with JUnit 5.
  • @TestInstance(TestInstance.Lifecycle.PER_CLASS) ensures that the container is shared across test methods within the class.
  • @Container creates a Container using the MySQL container instance.
  • In setup(), the mapped port of the MySQL container is set as a system property to configure the database connection URL.
  • Then, the contextLoads() method creates an EntityManager, persists a Customer entity, and fetches all customers from the database using JPA.

Finally, the Model Class which we use to fetch and insert data is the following Entity:

@Entity
public class Customer implements Serializable {

   @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  @Column
  private String email;

  @Column
  private String firstName;

  @Column
  private String lastName;

 // getters/setters omitted for brevity
}

Adding the JPA Configuration

Finally, we will add the persistence.xml file to define the Connection settings that JPA will use to create the Entity Manager:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
  <persistence-unit name="my-persistence">
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
 
    <properties>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:${db.port}/testdb?createDatabaseIfNotExist=true"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="javax.persistence.jdbc.password" value="password"/>
      <property name ="hibernate.show_sql" value = "true" />
      <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>

    </properties>

  </persistence-unit>
</persistence>

Notice that we need to use the same Database settings that we declare in the TestContainer Class.

Here is the Project view which shows where you need to place each file:

Testcontainers with Hibernate and JPA

Testing our application

To Test our TestContainer Class simply run the Maven install goal and verify that the Customer Entity displays on the Console:

[INFO] Running com.mastertheboss.TestContainerJPA
2023-11-30_18:42:25.113 INFO  org.testcontainers.images.PullPolicy - Image pull policy will be performed by: DefaultPullPolicy()
2023-11-30_18:42:25.117 INFO  o.t.utility.ImageNameSubstitutor - Image name substitution will be performed by: DefaultImageNameSubstitutor (composite of 'ConfigurationFileImageNameSubstitutor' and 'PrefixingImageNameSubstitutor')
2023-11-30_18:42:25.274 INFO  o.t.d.DockerClientProviderStrategy - Loaded org.testcontainers.dockerclient.UnixSocketClientProviderStrategy from ~/.testcontainers.properties, will try it first
2023-11-30_18:42:25.432 INFO  o.t.d.DockerClientProviderStrategy - Found Docker environment with local Unix socket (unix:///var/run/docker.sock)
2023-11-30_18:42:25.434 INFO  o.testcontainers.DockerClientFactory - Docker host IP address is localhost
2023-11-30_18:42:25.444 INFO  o.testcontainers.DockerClientFactory - Connected to docker: 
  Server Version: 20.10.22
  API Version: 1.41
  Operating System: Fedora Linux 35 (Workstation Edition)
  Total Memory: 63969 MB
2023-11-30_18:42:25.478 INFO  tc.testcontainers/ryuk:0.5.1 - Creating container for image: testcontainers/ryuk:0.5.1
2023-11-30_18:42:25.625 INFO  tc.testcontainers/ryuk:0.5.1 - Container testcontainers/ryuk:0.5.1 is starting: 74c290419ff42ea2c5ff7f876964da77811ffad8ffbcdae3085b1ca4e5d5. . . . .
Hibernate: insert into Customer (email,firstName,lastName,removed) values (?,?,?,0)
Hibernate: select c1_0.id,c1_0.email,c1_0.firstName,c1_0.lastName from Customer c1_0 where c1_0.removed=0
[Customer [id=1, [email protected], firstName=John, lastName=Doe]]

Conclusion

Using Testcontainers with JPA enables integration testing against a containerized database, ensuring your JPA entities, repositories, and database interactions work correctly.

Feel free to expand these tests to cover more scenarios, such as testing different CRUD operations, relationships between entities, and transactional behavior.

Source code: https://github.com/fmarchioni/mastertheboss/tree/master/test/testcontainer-jpa