Course topics

By WebNest Studio

Spring Boot Tutorial

Data Layer Tests with @DataJpaTest

Repository methods look trivial — until a derived query name matches the wrong property, a JPQL join drops rows, a projection maps a column incorrectly, or an entity mapping does not match the migration scripts. These bugs only appear when real SQL runs against a real schema.

@DataJpaTest starts just the persistence slice — entities, repositories, the EntityManager, Flyway or Liquibase — and wraps each test in a transaction that rolls back afterwards. This lesson shows how to test repositories, custom queries and mappings, why testing against your real database with Testcontainers beats H2, and how to use TestEntityManager.

What @DataJpaTest Configures

The slice includes JPA repositories, entity scanning, the DataSource, transaction management, migrations and a TestEntityManager. It does not load controllers or services. Each test method runs in a transaction that is rolled back at the end, so tests do not affect each other. SQL logging is enabled by default so you can see what ran. In Spring Boot 4, add spring-boot-starter-data-jpa-test with test scope.

H2 vs Your Real Database

By default @DataJpaTest replaces your DataSource with an embedded database such as H2. That is fast, but H2 is not PostgreSQL or MySQL: native queries, JSON columns, specific functions, case sensitivity and constraint behaviour differ, so tests can pass on H2 and fail in production. Prefer running the slice against the same database engine with Testcontainers and @ServiceConnection.

Flushing and Clearing

Within one transaction, Hibernate may keep changes in memory and never send SQL until flush. A test that saves an entity and immediately reads it back may only be testing the first-level cache. Use TestEntityManager.persistAndFlush() and clear() — or saveAndFlush() plus entityManager.clear() — so the read really hits the database. This is also how you catch constraint violations in tests.

What Deserves a Repository Test

Do not test Spring Data's own save and findById. Test your custom queries: derived methods with several conditions, @Query JPQL and native SQL, projections, specifications, pagination and sorting, and entity mappings such as cascades, unique constraints and soft deletes.

Examples

Test dependencies for data slice tests with Testcontainers

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-postgresql</artifactId>
    <scope>test</scope>
</dependency>
Output
(Testcontainers 2 renamed its modules to testcontainers-<module>; Spring Boot 4 manages their versions.)

Repository test against real PostgreSQL

Java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:17");

    @Autowired OrderRepository orders;
    @Autowired TestEntityManager em;

    Customer asha;

    @BeforeEach
    void setUp() {
        asha = em.persist(new Customer("asha@webnest.in", "Asha"));
        Customer ravi = em.persist(new Customer("ravi@webnest.in", "Ravi"));
        em.persist(new Order(asha, OrderStatus.PAID));
        em.persist(new Order(asha, OrderStatus.CANCELLED));
        em.persist(new Order(ravi, OrderStatus.PAID));
        em.flush();
        em.clear();          // make the queries below hit the database
    }

    @Test
    void findsOnlyMatchingOrdersForCustomer() {
        List<Order> result = orders.findForCustomer("asha@webnest.in", List.of(OrderStatus.PAID));

        assertThat(result)
            .hasSize(1)
            .allSatisfy(o -> assertThat(o.getStatus()).isEqualTo(OrderStatus.PAID));
    }

    @Test
    void cancelsStalePendingOrders() {
        em.persist(new Order(asha, OrderStatus.PENDING, Instant.parse("2026-01-01T00:00:00Z")));
        em.flush();

        int updated = orders.cancelStalePendingOrders(Instant.parse("2026-06-01T00:00:00Z"));

        assertThat(updated).isEqualTo(1);
    }
}
Output
Creating container for image: postgres:17
Container postgres:17 started in PT2.3S
Successfully applied 5 migrations to schema "public"
OrderRepositoryTest
  ✔ findsOnlyMatchingOrdersForCustomer()
  ✔ cancelsStalePendingOrders()
(each test rolled back automatically)

Testing constraints and projections

Java
@Test
void emailMustBeUnique() {
    em.persistAndFlush(new Customer("meera@webnest.in", "Meera"));

    assertThatThrownBy(() -> em.persistAndFlush(new Customer("meera@webnest.in", "Other Meera")))
        .isInstanceOf(ConstraintViolationException.class);   // org.hibernate.exception
}

@Test
void courseStatsProjectionAggregatesEnrollments() {
    Course boot = em.persist(new Course("Spring Boot"));
    Student s1 = em.persist(new Student("s1"));
    Student s2 = em.persist(new Student("s2"));
    em.persist(new Enrollment(s1, boot, 40));
    em.persist(new Enrollment(s2, boot, 60));
    em.flush();
    em.clear();

    assertThat(courseRepository.courseStats())
        .containsExactly(new CourseStats("Spring Boot", 2, 50.0));
}
Output
✔ emailMustBeUnique()
  ERROR: duplicate key value violates unique constraint "customers_email_key"
✔ courseStatsProjectionAggregatesEnrollments()

Common Mistakes

  • Testing with H2 while production runs PostgreSQL, so native queries and constraints are never really tested.
  • Saving and reading back without flush/clear, testing Hibernate's cache instead of the database.
  • Writing tests for inherited CRUD methods instead of your custom queries.
  • Sharing mutable data across tests and relying on execution order.
  • Starting a new container for every test class; declare it static (or in a shared @TestConfiguration) to reuse it.

Key Points to Remember

  • @DataJpaTest loads only the persistence layer and rolls back each test.
  • In Spring Boot 4 use spring-boot-starter-data-jpa-test.
  • Run repository tests against the real database engine using Testcontainers and @ServiceConnection.
  • Flush and clear the persistence context before asserting on query results.
  • Focus tests on custom queries, projections and mapping constraints.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.