Every Java application talks to a database. Choosing the right testing strategy for that interaction determines how much confidence your test suite actually gives you. This guide compares the three main approaches β H2 in-memory, real database, and Testcontainers β with honest trade-offs, code examples for each, and a practical guide to combining them for maximum coverage at minimum cost.
The Three Approaches at a Glance
| H2 In-Memory | Real Database (shared) | Testcontainers | |
|---|---|---|---|
| Speed | Fastest (<1s startup) | No startup cost | Medium (3β5s startup) |
| Realism | Low β H2 β PostgreSQL | High β same as production | Highest β exact same engine |
| Isolation | Perfect β fresh DB per run | Poor β shared state | Perfect β fresh container |
| CI friendly | Yes | Requires shared DB server | Yes (Docker required) |
| Catches dialect bugs | No | Yes | Yes |
| Setup complexity | Minimal | Medium (infra required) | Low (just Docker) |
Option 1: H2 In-Memory Database
H2 is an embedded Java database that starts in milliseconds. Spring Bootβs @DataJpaTest uses H2 by default if itβs on the classpath.
<!-- H2 in-memory: auto-detected by @DataJpaTest -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.beans.factory.annotation.Autowired;
// @DataJpaTest uses H2 by default β no configuration needed
@DataJpaTest
@DisplayName("ProductRepository β H2 in-memory tests")
class ProductRepositoryH2Test {
@Autowired
private TestEntityManager entityManager;
@Autowired
private ProductRepository productRepository;
@Test
@DisplayName("Saving a product persists it and assigns an ID")
void savingProductPersistsAndAssignsId() {
Product product = new Product(null, "Laptop", 999.00, "Electronics");
Product saved = entityManager.persistAndFlush(product);
assertNotNull(saved.getId(), "Auto-generated ID must be assigned");
assertEquals("Laptop", saved.getName());
}
@Test
@DisplayName("Custom query findByCategory returns matching products")
void findByCategoryReturnsMatchingProducts() {
entityManager.persistAndFlush(new Product(null, "Laptop", 999.0, "Electronics"));
entityManager.persistAndFlush(new Product(null, "Headphones", 79.0, "Electronics"));
entityManager.persistAndFlush(new Product(null, "Notebook", 5.9, "Stationery"));
List<Product> electronics = productRepository.findByCategory("Electronics");
assertEquals(2, electronics.size());
}
}
When to use H2: JPQL queries, basic CRUD verification, entity mapping validation, and anything that does not depend on database-specific SQL features or constraints.
When NOT to use H2: Native SQL queries, PostgreSQL-specific types (JSONB, arrays, UUID), CTEs, window functions, specific index behaviour, or ON CONFLICT (upsert) syntax.
Continue reading Database Testing in JUnit 6: H2 vs Real DB vs Containers