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
- Then, the TestContainer dependency for the Database you are going to Test
Finally, it’s a good idea to provide a BOM for TestContainers in order to avoid setting the individual dependencies in it:
<dependencyManagement> <dependencies> <dependency> <groupId>org.testcontainers</groupId> <artifactId>testcontainers-bom</artifactId> <version>1.20.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
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 testCreateCustomer() { 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(); entityManager.close(); // Verify EntityManager em = emf.createEntityManager(); List<Customer> customers = em.createQuery("SELECT c FROM Customer c", Customer.class).getResultList(); Assertions.assertEquals(1, customers.size()); Assertions.assertEquals("John", customers.get(0).getFirstName()); em.close(); } @Test void testDeleteCustomer() { EntityManager entityManager = emf.createEntityManager(); entityManager.getTransaction().begin(); Customer c = new Customer(); c.setFirstName("Charlie"); c.setLastName("Black"); c.setEmail("[email protected]"); entityManager.persist(c); entityManager.getTransaction().commit(); // Delete entityManager.getTransaction().begin(); entityManager.remove(entityManager.contains(c) ? c : entityManager.merge(c)); entityManager.getTransaction().commit(); entityManager.close(); // Verify EntityManager em = emf.createEntityManager(); Customer deletedCustomer = em.find(Customer.class, c.getId()); Assertions.assertNull(deletedCustomer); em.close(); } }
- 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 two Test methods verify creating, fetching and deleting a Customer using the
EntityManager
interface.
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="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:${db.port}/testdb?createDatabaseIfNotExist=true"/> <property name="jakarta.persistence.jdbc.user" value="root"/> <property name="jakarta.persistence.jdbc.password" value="password"/> <property name ="hibernate.show_sql" value = "true" /> <property name="jakarta.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:
Testing our application
To Test our TestContainer Class simply run the Maven install goal and verify that the Customer Entity displays on the Console:
Hibernate: create table Customer (id integer not null auto_increment, email varchar(255), firstName varchar(255), lastName varchar(255), primary key (id)) engine=InnoDB 2024-08-04_16:05:28.587 INFO org.hibernate.orm.connections.access - HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@54387873] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode. Hibernate: insert into Customer (email,firstName,lastName) values (?,?,?) Hibernate: select c1_0.id,c1_0.email,c1_0.firstName,c1_0.lastName from Customer c1_0 Hibernate: insert into Customer (email,firstName,lastName) values (?,?,?) Hibernate: delete from Customer where id=? Hibernate: select c1_0.id,c1_0.email,c1_0.firstName,c1_0.lastName from Customer c1_0 where c1_0.id=?
Customizing the Database settings
This example uses the default Database settings which can be customized in many ways. For example:
- Setting Initialization Scripts: Run custom SQL scripts on container startup to set up the database schema or insert initial data.
- Custom Configuration: Pass custom MySQL configuration parameters to the container.
- Exposing Additional Ports: Expose more ports if your application needs to interact with MySQL through different ports.
- Setting Environment Variables: Configure environment variables to customize MySQL’s behavior.
- Changing MySQL Version: Specify the version of MySQL you want to use.
- Network Configuration: Connect the MySQL container to a custom network if your tests involve multiple containers.
For example, you can customize some of these options during the creation of the container as in this example:
@Container private static final MySQLContainer MYSQL_CONTAINER = new MySQLContainer("mysql:8.0.28") .withDatabaseName("testdb") .withUsername("root") .withPassword("password") .withInitScript("init_mysql.sql") // Custom initialization script .withEnv("MYSQL_ROOT_HOST", "%") // Allow root access from any host .withExposedPorts(3306, 33060) // Expose additional ports .withCommand("--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"); // Custom MySQL configuration }
Place the init_mysql.sql
file in the src/test/resources
directory. Here’s an example init_mysql.sql
file:
CREATE TABLE Customer ( id INT AUTO_INCREMENT PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50), email VARCHAR(100) ); INSERT INTO Customer (first_name, last_name, email) VALUES ('Initial', 'User', '[email protected]');
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