Writing good unit tests for your central business logic is essential. Both to speed up your development and have confident deployments to production. But there are also parts of your application where plain unit tests with mocking frameworks like Mockito
aren't that useful or result in a “mocking-hell”. The interaction of your application with your database is one of these parts where unit tests can't reproduce the real behavior of this external system. For unit testing your business logic, it's totally fine to mock the EntityManager
and its result but if you want to write tests for your JPA models, JPQL queries or the general interaction with the database, you should consider writing integration tests. In this blog post, I’ll show a simple way to write JPA integration tests for Java EE applications.
Project setup
To bootstrap the Java EE application I am using my Java EE 8 archetype:
1 2 3 4 5 6 |
mvn archetype:generate -DarchetypeGroupId=de.rieckpil.archetypes \ -DarchetypeArtifactId=javaee8 \ -DarchetypeVersion=1.0.1 \ -DgroupId=de.rieckpil.blog \ -DartifactId=javaee-8-microservice \ -DinteractiveMode=false |
For integration tests, we don’t want to run the application server to improve speed. That's why you have to add the JPA implementation to your project. In addition, I am also using an embedded H2 database to spin off an in-memory database during the tests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.rieckpil.blog</groupId> <artifactId>jpa-integration-tests-java-ee</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.eclipse.microprofile</groupId> <artifactId>microprofile</artifactId> <version>2.0.1</version> <type>pom</type> <scope>provided</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.197</version> <scope>test</scope> </dependency> <dependency> <groupId>org.eclipse.persistence</groupId> <artifactId>eclipselink</artifactId> <version>2.7.3</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.23.0</version> <scope>test</scope> </dependency> </dependencies> <build> <finalName>jpa-integration-tests-java-ee</finalName> </build> <properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> </project> |
All of the additional dependencies are marked with the scope test
, so they won’t be part of your .war
and the application will stay lean.
For connection to the in-memory, you need to add a persistence unit to your persistence.xml
which can be stored in /src/main/test/resources/META-INF
. As these tests don’t rely on a running application server, the transaction type has to be RESOURCE_LOCAL
as we don’t have JTA and have to manually start and commit the transactions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="UTF-8"?> <persistence version="2.2" 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_2.xsd"> <persistence-unit name="integration-test" transaction-type="RESOURCE_LOCAL"> <class>sample.Customer</class> <properties> <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test:sample;DB_CLOSE_ON_EXIT=FALSE;" /> <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" /> <property name="javax.persistence.schema-generation.database.action" value="drop-and-create" /> <property name="eclipselink.logging.level" value="FINE"/> </properties> </persistence-unit> </persistence> |
To demonstrate a quick example I am using drop-and-create
as the schema generation action for the integration tests, but you can also use Flyway or Liquibase to apply your database schema for your integration tests.
JPA entity setup and writing efficient tests
A simple JPA entity may look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String firstName; private String lastName; private LocalDate dayOfBirth; // getter, setter & constructors } |
For a convenient interaction, I created a JUnit 4 TestRule
(kudos to @AdamBien for his test course) which acts as the EntityManager provider for every test. With this rule, you can create the EntityManager from a given unit name. Furthermore, it allows interaction with the EntityManager or the EntityTransaction:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
public class EntityManagerProvider implements TestRule { private EntityManager em; private EntityTransaction tx; private EntityManagerProvider(String unitName) { EntityManagerFactory emf = Persistence.createEntityManagerFactory(unitName); this.em = emf.createEntityManager(); this.tx = em.getTransaction(); } public static EntityManagerProvider withUnit(String unitName) { return new EntityManagerProvider(unitName); } public void begin() { this.tx.begin(); } public void commit() { this.tx.commit(); } public EntityTransaction tx() { return this.tx; } public EntityManager em() { return this.em; } @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { base.evaluate(); em.clear(); } }; } } |
A rudimentary integration test might test the right creation of the primary key with @GeneratedValue
. Therefore you need to start the database transaction manually and insert some JPA entities. Afterward querying for all entities in the database should result in a list of entities. Similarly, all should have a primary key in place.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class CustomerIntegrationTest { @Rule public EntityManagerProvider provider = EntityManagerProvider.withUnit("integration-test"); @Test public void testSavingNewCustomer() { this.provider.begin(); this.provider.em().persist(new Customer("John", "Duke", LocalDate.of(2000, 12, 12))); this.provider.em().persist(new Customer("Foo", "Bar", LocalDate.of(2000, 12, 12))); this.provider.em().persist(new Customer("Paul", "One", LocalDate.of(2000, 12, 12))); List<Customer> resultList = this.provider.em() .createQuery("SELECT c FROM Customer c", Customer.class) .getResultList(); assertEquals(3, resultList.size()); for (Customer resultCustomer : resultList) { assertNotNull(resultCustomer.getId()); } this.provider.commit(); } } |
This example is just a simple use case. It demonstrates how to to write integration tests for your database layer. This approach can be also used to test the correct table representation of your JPA entities and more complex queries.
You can find a running example on GitHub.
Further resources on JPA
Find further persistence related tips & tricks here:
- Best Practices for Flyway and Hibernate with Spring Boot
- Lazy Loading of JPA attributes with Hibernate
- Avoid repeating attributes in JPA entities
Have fun with JPA integration tests for your application,
Philip