@DataJpaTest

Starts only JPA, your repositories and a test database, wrapping each test in a transaction that rolls back, to test queries and mappings.

Signature
@DataJpaTest(showSql = false)
  • Added in Spring Boot Spring Boot 1.4

Parameters

ParameterTypeDescription
$propertiesoptionalString[]= {}key=value pairs added to the Environment for this test.
$showSqloptionalboolean= trueMaps to spring.jpa.show-sql: whether SQL statements are logged.
$bootstrapModeoptionalBootstrapMode= DEFAULTMaps to spring.data.jpa.repositories.bootstrap-mode: DEFAULT, DEFERRED or LAZY repository initialisation.
$useDefaultFiltersoptionalboolean= trueWhether to use the default filter that limits scanning to JPA types.
$includeFiltersoptionalComponentScan.Filter[]= {}Extra types to add to the slice.
$excludeFiltersoptionalComponentScan.Filter[]= {}Types to remove from the slice.
$excludeAutoConfigurationoptionalClass<?>[]= {}Auto-configuration classes to leave out.

Return value

n/aStarts a JPA test slice: DataSource, Hibernate, Spring Data repositories and TestEntityManager, with each test in a transaction rolled back at the end.

Errors

Constraint violations surface as DataIntegrityViolationException only when the change is flushed.

Changelog

  • PHP Spring Boot 4.0Moved to org.springframework.boot.data.jpa.test.autoconfigure in the spring-boot-data-jpa-test module (spring-boot-starter-data-jpa-test).

@DataJpaTest is the persistence slice. It configures a DataSource, Hibernate, Spring Data JPA repositories, Flyway or Liquibase if present, and a TestEntityManager. Controllers, services and other components are not loaded. Every test method runs in a transaction that is rolled back at the end, so tests do not see each other's rows.

In Spring Boot 4 it is org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest, from spring-boot-starter-data-jpa-test. TestEntityManager moved to org.springframework.boot.jpa.test.autoconfigure.

Which database it uses

The slice is meta-annotated with @AutoConfigureTestDatabase, whose default replace mode is NON_TEST. If an embedded database such as H2 is on the classpath, it replaces the DataSource from your configuration. A DataSource that comes from the test itself, such as a Testcontainers database with @ServiceConnection, is kept. Use replace = Replace.NONE to keep the configured database in every case.

Hibernate only creates the schema by itself for an embedded database. Against PostgreSQL the default ddl-auto is none, so the tables must come from your Flyway or Liquibase migrations (which the slice runs) or from spring.jpa.hibernate.ddl-auto=create-drop set for the test. Otherwise the first query fails with relation does not exist.

Prefer the real engine. H2 does not have PostgreSQL's jsonb, its locking or its exact SQL dialect, so a query that passes on H2 can fail in production. A PostgreSQL container started once per test class costs a few seconds and removes that whole class of surprise.

The flush trap

Hibernate writes changes at flush time, usually at commit. Because the test transaction never commits, a plain save() may never send an INSERT: unique constraints, not-null columns and database defaults are not checked, and the test passes when production would fail. Call saveAndFlush() or em.flush() when the test is about what the database accepts. Call em.clear() before reading back, otherwise you get the same Java object from the persistence context rather than what was stored.

What to test here

Derived and @Query methods with non-trivial conditions, projections, fetch joins, mapping of embeddables and converters, and constraints. Do not test that save followed by findById works; Spring Data already does. The rollback also hides LazyInitializationException, because the session stays open for the whole test, so test lazy loading through a service in a @SpringBootTest instead.

Example

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
import org.springframework.data.jpa.repository.JpaRepository;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
class Book {

    @Id
    @GeneratedValue
    Long id;

    String title;
    String author;
    boolean archived;

    protected Book() {}

    Book(String title, String author, boolean archived) {
        this.title = title;
        this.author = author;
        this.archived = archived;
    }
}

interface BookRepository extends JpaRepository<Book, Long> {
    List<Book> findByAuthorAndArchivedFalseOrderByTitle(String author);
}

// JPA, the repositories and an embedded database; each test rolls back.
@DataJpaTest
class BookRepositoryTest {

    @Autowired
    TestEntityManager em;

    @Autowired
    BookRepository books;

    @Test
    void findsOnlyLiveBooksByAuthorInTitleOrder() {
        em.persist(new Book("Persuasion", "Austen", false));
        em.persist(new Book("Emma", "Austen", false));
        em.persist(new Book("Lady Susan", "Austen", true));
        em.persist(new Book("Middlemarch", "Eliot", false));
        em.flush();
        em.clear(); // read from the database, not the persistence context

        List<Book> found = books.findByAuthorAndArchivedFalseOrderByTitle("Austen");

        assertThat(found).extracting(b -> b.title).containsExactly("Emma", "Persuasion");
    }
}

When to use it

  • Check that a derived query with three conditions and a sort returns exactly the rows a search screen expects.
  • Prove that a duplicate email is rejected by the unique index before relying on it in the sign-up service.
  • Catch a native query that works on H2 but uses syntax PostgreSQL rejects.

Frequently asked questions

Why does @DataJpaTest use H2 when I configured PostgreSQL?

The default replace mode swaps a configured DataSource for an embedded database when one is on the classpath. Supply the database from the test with a Testcontainers container and @ServiceConnection, or add @AutoConfigureTestDatabase(replace = Replace.NONE). Against the container, let your Flyway migrations create the schema, or set spring.jpa.hibernate.ddl-auto=create-drop for the test, because Hibernate only creates tables automatically for an embedded database.

Can I stop the rollback?

Add @Rollback(false) or @Commit to a test method, or @Transactional(propagation = Propagation.NOT_SUPPORTED) to run without a test transaction. Then clean up yourself, because rows will persist between tests.

Are my services available in a @DataJpaTest?

No. Only JPA infrastructure and repositories are loaded. @Import a service if you need it, or move the test to @SpringBootTest when the service's own transactions are what you are testing.

More examples

Running the slice against real PostgreSQL

The static container starts once for the class. <code>@ServiceConnection</code> supplies the connection details, and because that <code>DataSource</code> comes from the test, the default <code>NON_TEST</code> replace mode leaves it alone instead of swapping in H2. PostgreSQL is not an embedded database, so Hibernate does not create tables unless told to: in a real project Flyway migrations do that, here <code>ddl-auto=create-drop</code> does. <code>@JdbcTypeCode(SqlTypes.JSON)</code> makes Hibernate bind the value as JSON; without it the insert fails because PostgreSQL will not put a <code>varchar</code> into a <code>jsonb</code> column. The <code>jsonb</code> column would not even create on H2, which is the point of using the real engine.

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import org.springframework.data.jpa.repository.JpaRepository;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.postgresql.PostgreSQLContainer;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
class Customer {

    @Id
    @GeneratedValue
    Long id;

    // Bind as JSON; a plain String is sent as varchar, which PostgreSQL rejects for jsonb.
    @JdbcTypeCode(SqlTypes.JSON)
    @Column(columnDefinition = "jsonb")
    String preferences;

    protected Customer() {}

    Customer(String preferences) {
        this.preferences = preferences;
    }
}

interface CustomerRepository extends JpaRepository<Customer, Long> {}

// The same database engine as production, started for this class.
// No migrations in this example, so Hibernate creates the tables;
// with Flyway on the classpath, drop the property and let the migrations run.
@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=create-drop")
@Testcontainers
class CustomerRepositoryPostgresTest {

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

    @Autowired
    CustomerRepository customers;

    @Test
    void storesJsonbColumns() {
        Customer saved = customers.saveAndFlush(new Customer("{\"newsletter\": true}"));
        assertThat(customers.findById(saved.id)).isPresent();
    }
}

Proving a unique constraint with saveAndFlush

The second insert must reach the database for the constraint to fire. With <code>save()</code> the entity would only be queued and the transaction rolled back before flushing, so the test would wrongly pass. Spring translates the driver's error into <code>DataIntegrityViolationException</code>, the same exception your service sees in production.

Example · java
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.jpa.repository.JpaRepository;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;

@Entity
class Account {

    @Id
    @GeneratedValue
    Long id;

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

    protected Account() {}

    Account(String email) {
        this.email = email;
    }
}

interface AccountRepository extends JpaRepository<Account, Long> {}

@DataJpaTest
class AccountConstraintsTest {

    @Autowired
    AccountRepository accounts;

    @Test
    void duplicateEmailIsRejectedByTheDatabase() {
        accounts.saveAndFlush(new Account("[email protected]"));

        // save() alone would pass: the INSERT only happens at flush,
        // and the test transaction is rolled back before that.
        assertThatThrownBy(() -> accounts.saveAndFlush(new Account("[email protected]")))
                .isInstanceOf(DataIntegrityViolationException.class);
    }
}

Validating mappings against the configured database

<code>Replace.NONE</code> keeps the <code>DataSource</code> from configuration, for example a database that Flyway has migrated. <code>ddl-auto=validate</code> makes Hibernate compare each entity with the real tables at startup, so a renamed column fails here instead of on the first request in production. <code>showSql = false</code> turns off the SQL logging the slice enables by default.

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase.Replace;
import org.springframework.data.jpa.repository.JpaRepository;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
class Country {

    @Id
    String code;

    String name;

    protected Country() {}
}

interface CountryRepository extends JpaRepository<Country, String> {}

// Use the DataSource from configuration (for example a local Docker database),
// check the mappings against the migrated schema, and keep the log quiet.
@DataJpaTest(showSql = false, properties = "spring.jpa.hibernate.ddl-auto=validate")
@AutoConfigureTestDatabase(replace = Replace.NONE)
class CountryMappingTest {

    @Autowired
    CountryRepository countries;

    @Test
    void schemaMatchesTheEntities() {
        assertThat(countries.count()).isGreaterThanOrEqualTo(0);
    }
}

Discussion

  • Be the first to comment on this lesson.