Integration Testing with Hibernate
|
This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Three older reference books were consulted as bibliography only while preparing these pages and are not the
primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the
This section’s bibliography lists the reference material consulted while preparing these pages. |
Every page so far in this section assumes Hibernate is used on its own terms, independent of any framework — this page is no exception. It covers testing a Hibernate mapping/query against a real database engine
without Spring: bootstrapping a SessionFactory directly, the trade-offs of in-memory databases, running the
same test suite against several database engines with a parameterized test, and standing up real engines with
Testcontainers. If the application under test is a Spring Boot application, prefer the Spring-aware shortcuts
(@DataJpaTest, @ServiceConnection) on Unit &
Integration Testing instead — the patterns below are what those shortcuts are themselves built on.
Bootstrapping a test SessionFactory
Outside a framework, a test suite builds its own EntityManagerFactory/SessionFactory once per test class
(it is expensive to create — see Architecture) and reuses it across
test methods, wiping and re-seeding data between them instead of rebuilding it every time:
class BookRepositoryIT {
static EntityManagerFactory emf;
EntityManager em;
@BeforeAll
static void bootstrap() {
// reads META-INF/persistence.xml (a "test" persistence unit),
// or build one programmatically with PersistenceUnitInfo for full control over JDBC URL and dialect
emf = Persistence.createEntityManagerFactory("test");
}
@BeforeEach
void openSession() {
em = emf.createEntityManager();
}
@AfterEach
void closeSession() {
em.close();
}
@AfterAll
static void shutdown() {
emf.close();
}
@Test
void findByIsbn_returnsMatchingBook() {
em.getTransaction().begin();
em.persist(new Book("978-1", "Effective Java"));
em.getTransaction().commit();
Book found = em.createQuery("SELECT b FROM Book b WHERE b.isbn = :isbn", Book.class)
.setParameter("isbn", "978-1")
.getSingleResult();
assertThat(found.getTitle()).isEqualTo("Effective Java");
}
}
hibernate.hbm2ddl.auto=create-drop (test-only — never in production, see
Schema Generation and Tooling) rebuilds the schema
for every test run so tests never depend on leftover state from a previous run.
In-memory databases — fast, but not the production dialect
An embedded, in-process database — H2 (the most common choice with Hibernate), HSQLDB, or Derby — starts in milliseconds with no external process, which is why it is the default choice for a fast unit-level test suite. Add it as a test-scoped dependency — no separate server or driver install needed, the JDBC driver ships in the same artifact:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.4.240</version>
<scope>test</scope>
</dependency>
hibernate.connection.driver_class=org.h2.Driver
hibernate.connection.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
hibernate.dialect=org.hibernate.dialect.H2Dialect
The trade-off is real, not theoretical: an in-memory engine is a different database product from
PostgreSQL/MySQL/Oracle in production, with its own SQL dialect, its own function library, its own locking and
isolation implementation, and its own handling of vendor-specific column types. A query that works against H2
can fail, or silently behave differently, against the production engine — window functions, RETURNING
clauses, JSON column functions, and case-sensitivity rules are common places this shows up. Treat an in-memory
database as a fast smoke test for mapping/query shape, not as a substitute for testing against the real
engine before trusting a query in production — the same principle
Unit & Integration Testing's "Choosing the right
level" section already states for the Spring Boot stack.
Testing multiple database engines with parameterized tests
When a codebase must run correctly against more than one database engine (a library, or an application that
supports customer choice of database), a single hard-coded SessionFactory cannot catch dialect-specific
divergence. JUnit 5’s @ParameterizedTest runs the same test body once per supplied database configuration,
each rebuilding the SessionFactory against a different JDBC URL and Hibernate dialect:
class BookRepositoryDialectIT {
record DbConfig(String name, String jdbcUrl, String username, String password, String dialect) {
@Override
public String toString() {
return name; // shown as the parameterized test's display name
}
}
static Stream<DbConfig> databases() {
return Stream.of(
new DbConfig("H2", "jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", "sa", "",
"org.hibernate.dialect.H2Dialect"),
new DbConfig("PostgreSQL", postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword(),
"org.hibernate.dialect.PostgreSQLDialect"),
new DbConfig("MySQL", mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword(),
"org.hibernate.dialect.MySQLDialect"));
}
@ParameterizedTest(name = "{0}")
@MethodSource("databases")
void findByIsbn_returnsMatchingBook(DbConfig db) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("test", Map.of(
"hibernate.connection.url", db.jdbcUrl(),
"hibernate.connection.username", db.username(),
"hibernate.connection.password", db.password(),
"hibernate.dialect", db.dialect(),
"hibernate.hbm2ddl.auto", "create-drop"));
try (emf) {
// same test body as the single-engine version above
}
}
}
Overriding hibernate.connection.*/hibernate.dialect as a properties map when building the
EntityManagerFactory (rather than editing persistence.xml per engine) is what makes the same persistence
unit reusable across parameters. This pattern is what surfaces dialect divergence (a query that only works on
one engine) in CI, in exactly the case an in-memory-only suite would miss per the previous section.
Testcontainers with Docker images
For any engine that has no credible in-memory equivalent — or to remove the in-memory/production dialect gap entirely — Testcontainers starts the real engine as a disposable Docker container for the duration of the test run:
@Testcontainers
class BookRepositoryPostgresIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
static EntityManagerFactory emf;
@BeforeAll
static void bootstrap() {
emf = Persistence.createEntityManagerFactory("test", Map.of(
"hibernate.connection.url", postgres.getJdbcUrl(),
"hibernate.connection.username", postgres.getUsername(),
"hibernate.connection.password", postgres.getPassword(),
"hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect",
"hibernate.hbm2ddl.auto", "create-drop"));
}
// test methods as above
}
The container starts once (a static field, shared across the class’s test methods via the @Testcontainers
JUnit 5 extension) and is torn down automatically when the JVM exits. Combine this with the parameterized
pattern above — one PostgreSQLContainer/MySQLContainer/other module per engine under test, each contributing
one DbConfig — to run the full suite against every supported engine in CI without maintaining separate
database servers. Without Spring Boot’s @ServiceConnection (Spring-specific, see
Unit & Integration Testing), the container’s JDBC
URL/username/password are read directly off the container instance and passed into the EntityManagerFactory
properties, as shown above.
Links
-
H2 Database — Features (in-memory mode, compatibility modes)
-
Schema Generation and Tooling — why
create-drop/updateare test-only, never production, settings. -
Unit & Integration Testing — the Spring Boot-specific shortcuts (
@DataJpaTest,@ServiceConnection) built on the same underlying patterns. -
Integration Testing with Hibernate Search — the same in-memory-first/Testcontainers-for-parity approach applied to search: analyzers, queries, faceting, and projections.