Test a Spring Data JPA Repository with @DataJpaTest

Test Spring Data JPA repositories with @DataJpaTest on Spring Boot 4.1.2. Run H2 query tests, verify rollback behavior, and see how to keep a configured datasource.

Spring DataJpaTest Annotation

Use @DataJpaTest when a test needs real JPA mapping and repository queries without starting the full Spring Boot application. The annotation loads entities, JPA repositories, and the related database configuration. With H2 on the test classpath, the slice uses an embedded database. Each test runs in a transaction that rolls back by default, so test data does not carry over to the next test. An H2 test still cannot prove that a query behaves identically on a different production database.

1. What the JPA Test Slice Loads

A JPA slice creates the beans needed to test persistence: the JPA entity manager, repository implementations and a datasource. It excludes ordinary application services and web controllers. That makes it a good fit for checking a repository query, but it cannot verify an API request through the controller or a service method that the slice did not load.

@DataJpaTest(showSql = false)
class PurchaseOrderRepositoryTest {
    @Autowired TestEntityManager entityManager;
    @Autowired PurchaseOrderRepository repository;
}

TestEntityManager is a test-oriented wrapper around JPA’s EntityManager. We will use it to insert data, flush pending SQL and clear managed entities before invoking the repository. showSql = false quiets the SQL output; SQL logging is otherwise enabled by default for this slice. The Spring Boot testing reference lists the slice configuration and its rollback behavior. For a test that must cross the controller, service and repository layers, see the Spring Boot integration testing guide.

2. Create an Entity and Repository

An order application stores a unique business order number and the customer’s email address. Its repository must find one order by business number and list orders for a customer. These are application queries worth testing against real JPA, rather than mocking the repository interface.

The Maven project uses Spring Boot 4.1.2, Java 17, the JPA starter, H2 for tests, and Boot’s JPA test starter. The test dependency supplies the JUnit and JPA slice support used below:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

PurchaseOrder maps to the purchase_orders table. The generated numeric ID is the JPA identifier; orderNumber is the business value the application uses to look up an order. The protected no-argument constructor lets JPA create entities when reading rows.

@Entity
@Table(name = "purchase_orders")
public class PurchaseOrder {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String orderNumber;

    @Column(nullable = false)
    private String customerEmail;

    protected PurchaseOrder() { }

    public PurchaseOrder(String orderNumber, String customerEmail) {
        this.orderNumber = orderNumber;
        this.customerEmail = customerEmail;
    }

    public Long getId() { return id; }
    public String getOrderNumber() { return orderNumber; }
    public String getCustomerEmail() { return customerEmail; }
}

The repository extends JpaRepository for standard persistence operations. Spring Data derives the two lookup queries from the method names. The test will check that each method returns the expected rows, not merely that a repository bean was created.

public interface PurchaseOrderRepository extends JpaRepository<PurchaseOrder, Long> {
    Optional<PurchaseOrder> findByOrderNumber(String orderNumber);
    List<PurchaseOrder> findByCustomerEmail(String customerEmail);
}

3. Test the Repository Query

The first test inserts an order, then calls flush() and clear() before reading it through findByOrderNumber(). Flushing sends pending changes to the database within the test transaction. Clearing removes managed entities from the persistence context. The subsequent repository lookup must therefore hydrate an entity from the database rather than returning the instance the test already holds.

@DataJpaTest(showSql = false)
class PurchaseOrderRepositoryTest {
    @Autowired TestEntityManager entityManager;
    @Autowired PurchaseOrderRepository repository;

    @Test
    void findsOrderByItsBusinessNumber() {
        entityManager.persistAndFlush(
            new PurchaseOrder("PO-101", "one@example.com"));
        entityManager.clear();

        assertThat(repository.findByOrderNumber("PO-101"))
            .get().extracting(PurchaseOrder::getCustomerEmail)
            .isEqualTo("one@example.com");
    }
}

The result proves that the derived query finds the row identified by PO-101 and maps its email. It does not prove that the order was committed: the insert and select run in the same test transaction, which rolls back when the method finishes.

A second query should return only the customer’s orders. This test inserts two rows for one email and one row for another, then checks the exact business numbers returned by findByCustomerEmail().

@Test
void filtersOrdersByCustomerEmail() {
    entityManager.persist(new PurchaseOrder("PO-101", "one@example.com"));
    entityManager.persist(new PurchaseOrder("PO-102", "one@example.com"));
    entityManager.persist(new PurchaseOrder("PO-103", "two@example.com"));
    entityManager.flush();
    entityManager.clear();

    assertThat(repository.findByCustomerEmail("one@example.com"))
        .extracting(PurchaseOrder::getOrderNumber)
        .containsExactlyInAnyOrder("PO-101", "PO-102");
}

The assertion checks both inclusion and exclusion. If the query returned the third customer’s order as well, containsExactlyInAnyOrder() would fail.

4. Test a Missing Result and Isolation

A lookup by a nonexistent business number should return Optional.empty(). The repository method’s return type makes that result explicit to callers, and the test verifies the behavior without inventing an order row.

@Test
void returnsEmptyForUnknownOrderNumber() {
    assertThat(repository.findByOrderNumber("PO-999")).isEmpty();
}

The three tests can run in any order because @DataJpaTest rolls back each test method’s transaction by default. Flushing in the earlier tests does not commit those inserts. It only makes SQL execute inside the current transaction. If you disable transactions for a slice test, this isolation rule changes; arrange cleanup explicitly before relying on independent tests.

5. Choose the Test Database

With H2 available, @DataJpaTest replaces a configured datasource with an embedded one by default. The test run in the companion project logs that dataSource is replaced and starts an H2 in-memory URL. This gives fast repository feedback without a database service, but H2’s SQL dialect and constraints may differ from PostgreSQL or MySQL.

If a test must use the datasource supplied by your test configuration, add @AutoConfigureTestDatabase(replace = NONE). This opts out of replacement; it does not create or isolate a database for you. Point that datasource at a disposable test database before running a write test. The companion project tests this switch with a separately configured H2 URL. It checks the actual JDBC URL so the assertion would fail if the slice silently replaced that datasource. In an application that uses PostgreSQL or MySQL, supply a dedicated test instance of that engine instead.

@DataJpaTest(properties =
    "spring.datasource.url=jdbc:h2:mem:configured-db;DB_CLOSE_DELAY=-1")
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class ConfiguredDatabaseRepositoryTest {
    @Autowired DataSource dataSource;
    @Autowired PurchaseOrderRepository repository;

    @Test
    void usesTheConfiguredDataSource() throws SQLException {
        try (var connection = dataSource.getConnection()) {
            assertThat(connection.getMetaData().getURL())
                .contains("configured-db");
        }
        assertThat(repository.count()).isZero();
    }
}

Use the real database engine when repository behavior depends on vendor-specific SQL, migrations, constraints or transaction semantics. The Spring Boot Testcontainers guide shows one way to supply a disposable service database. Keep H2 tests for fast feedback on ordinary mappings and derived queries.

6. Run the Example

The companion Maven project uses Java 17 as its target. Run its tests from the project folder; the four tests should pass without Docker or a separately installed database.

mvn test

The verified run used Spring Boot 4.1.2 and Java 25 with Java 17 target settings. It found one repository, replaced the datasource with H2 and passed all four tests. The exact H2 in-memory URL is generated per test context and will differ on another run.

7. FAQs

7.1. Does @DataJpaTest test my service layer?

No. The slice loads JPA entities and repositories, not ordinary service beans. If the behavior you need to verify spans a service and repository, use a full-context test or explicitly import a narrow collaborator. Do not treat a repository result as proof that a service method’s rules were exercised.

7.2. Why does my configured database become H2?

When an embedded database is on the test classpath, the JPA slice replaces the configured datasource by default. Use @AutoConfigureTestDatabase(replace = NONE) with a dedicated test datasource when you need the configured database instead. Check the test startup log to confirm which JDBC URL actually started.

7.3. Does flush make test data survive rollback?

No. flush() sends pending SQL to the database inside the current transaction; it is not a commit. The default test rollback still removes those changes at the end of the method. clear() has a different role: it detaches entities from the persistence context so a later read must reconstruct them from database rows.

8. Conclusion

Use @DataJpaTest for focused entity mapping and repository query tests. H2 gives quick feedback, while tests against the production database engine are appropriate for dialect-specific behavior. Check the datasource chosen at startup and keep the default rollback in mind when interpreting a passing read.

Complete runnable Spring Boot 4.1.2 source

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.