Integration Testing with Hibernate Search
|
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. |
Integration Testing with Hibernate covers testing Hibernate ORM
itself — bootstrapping a test SessionFactory, in-memory databases, and Testcontainers for the database.
This page covers the analogous ground for Hibernate Search: mapping annotations, analyzers, queries, faceting
and projections all deserve their own tests, independent of whether the underlying entities are also
database-tested.
The Lucene backend’s in-memory directory — the H2 equivalent for search
Hibernate Search Backends covers the Lucene backend’s
production limitation (it does not scale across application instances). For a test suite, that same embedded
Lucene backend is exactly the right tool — and setting hibernate.search.backend.directory.type=local-heap
keeps the index entirely in the JVM heap, with no filesystem directory left behind between test runs, the same
role H2 plays for the database side:
hibernate:
search:
backend:
directory:
type: local-heap # index lives in the JVM heap; gone when the JVM exits -- no cleanup needed
As with H2 (Integration Testing with Hibernate's own "in-memory databases" caveat), this is a fast smoke test of mapping and query shape, not a guarantee of production parity — the Lucene backend’s own analysis chain and query execution differ in some respects from running the same mapping against a real Elasticsearch/OpenSearch cluster (see Hibernate Search Backends for how the two backends differ). Treat Lucene-backed tests as the fast, default layer, and reach for Testcontainers (below) for the smaller set of tests that must prove behavior against the actual production backend.
Testing custom analyzers
Hibernate Search Analyzers shows defining a
language-specific stemming analyzer and an n-gram-based autocomplete analyzer. Neither Hibernate Search’s
Mutiny-free synchronous API nor the Lucene backend expose analyzer tokenization as a public, directly
unit-testable method — so an analyzer is tested the same way the rest of the mapping is: index a small,
targeted set of documents built specifically to exercise the analyzer’s behavior, then assert on which queries
do and do not match:
@Test
void englishStemmingAnalyzer_matchesAcrossInflections() {
SearchSession searchSession = Search.session(entityManager);
entityManager.getTransaction().begin();
entityManager.persist(new Book("Effective Java", "Running a JVM smoothly", "en"));
entityManager.getTransaction().commit();
List<Book> hits = searchSession.search(Book.class)
.where(f -> f.match().field("description").matching("runs")) // stemmed query term
.fetchHits(20);
assertThat(hits).extracting(Book::getTitle).containsExactly("Effective Java"); // matched via "run" stem
}
@Test
void autocompleteAnalyzer_matchesPartialPrefix() {
// same shape: index a document through the "autocomplete" analyzer's field,
// then assert a partial query term ("hiber") matches "Hibernate" but "xyz" does not
}
This treats the analyzer as a black box exercised through the same SearchSession API application code uses — appropriate for Hibernate Search’s own analyzers, since asserting on Hibernate Search’s actual match behavior is
the thing that matters, not the raw token stream. If a pure, Hibernate-Search-independent unit test of the
tokenization itself is wanted (no index, no entity, just "does this analyzer chain produce the tokens I
expect"), that is a plain Apache Lucene Analyzer test instead — build the same LuceneAnalysisConfigurer’s
tokenizer/filter chain directly against Lucene’s own `Analyzer/TokenStream API, entirely decoupled from
Hibernate Search.
Testing queries, faceting, and projections
The same in-memory-backed SearchSession verifies the query DSL, aggregation, and projection behavior covered
on Hibernate Search Fundamentals:
@Test
void termsAggregation_countsHitsPerGenre() {
seedBooks(); // persists a handful of Book entities across a few genre values
AggregationKey<Map<String, Long>> countsByGenre = AggregationKey.of("countsByGenre");
SearchResult<Book> result = searchSession.search(Book.class)
.where(f -> f.matchAll())
.aggregation(countsByGenre, f -> f.terms().field("genre", String.class))
.fetch(20);
assertThat(result.aggregation(countsByGenre))
.containsEntry("Fiction", 2L)
.containsEntry("Non-Fiction", 1L);
}
@Test
void idProjection_returnsOnlyMatchingIdentifiers() {
seedBooks();
List<Long> ids = searchSession.search(Book.class)
.select(f -> f.id(Long.class))
.where(f -> f.match().field("title").matching("persistence"))
.fetchHits(20);
assertThat(ids).containsExactly(persistenceBookId);
}
Because indexing runs synchronously by default against the Lucene backend in a single-threaded test (see
Hibernate Search Backends's "Sync vs. async index
writes"), a search immediately after persist/commit sees the just-indexed documents without needing to poll or
sleep — a real convenience for tests that would otherwise need to wait for asynchronous indexing to settle.
Testcontainers with a real Elasticsearch/OpenSearch image
When a test must prove behavior against the actual production backend — an Elasticsearch-specific relevance tuning detail (see Elasticsearch for the query DSL and relevance concepts this backend runs), a coordination-strategy interaction, or simply a pre-release confidence check — Testcontainers' Elasticsearch module starts a real cluster as a disposable Docker container, the same pattern Integration Testing with Hibernate uses for the database:
@Testcontainers
class BookSearchElasticsearchIT {
@Container
static ElasticsearchContainer elasticsearch =
new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:9.0.0")
.withEnv("xpack.security.enabled", "false");
static EntityManagerFactory emf;
@BeforeAll
static void bootstrap() {
emf = Persistence.createEntityManagerFactory("test", Map.of(
"hibernate.search.backend.type", "elasticsearch",
"hibernate.search.backend.hosts", elasticsearch.getHttpHostAddress(),
"hibernate.search.backend.protocol", "http",
"hibernate.search.schema_management.strategy", "drop-and-create-and-drop"));
}
// test methods as above, unchanged -- the SearchSession/query DSL calls are backend-agnostic
}
elasticsearch.getHttpHostAddress() reads the running container’s actual host:port, the same role
postgres.getJdbcUrl() plays for a database container on
Integration Testing with Hibernate. Test methods written
against the query DSL, aggregations, and projections do not need to change between the Lucene-backed and
Elasticsearch-backed variants — only the EntityManagerFactory bootstrap properties differ — so it is common
to run the fast Lucene-backed suite on every build and the Elasticsearch-backed variant on a smaller, slower CI
job or before a release.
Links
-
Hibernate Search — Lucene backend directory types (including
local-heap) -
Integration Testing with Hibernate — the database-side counterpart this page’s patterns mirror.