Skip to content
CampusEduX

Testing · Lesson 79 of 95

@DataJpaTest

Use @DataJpaTest in Spring Boot to test JPA entities and repository queries on an in-memory H2 database with automatic rollback, with a library example.

8 min read

Before a library opens a new counter, the staff try it with a small trial shelf. They put a few books on it, search for a title, borrow one, and check the record. When they are done, they clear the trial shelf, so the real shelves are never touched. The trial is cheap, quick and safe.

@DataJpaTest gives your repositories the same kind of trial shelf. This guide on @DataJpaTest shows how to test your JPA entities and repository queries against a small in-memory database that is built and thrown away for each test.

What is @DataJpaTest?

Like @WebMvcTest for controllers, this is a slice test for the data layer. Your controllers and services are not created. Spring Boot replaces your real database with an embedded one, such as H2, and builds the tables from your entities.

You also get a TestEntityManager, a helper for putting test data in the database without using the repository you are testing.

Why is it used?

  • Tests real queries. A derived method like findByAuthorName is turned into real SQL, so a wrong property name is found at once.
  • Fast. Only the JPA pieces start, and the database lives in memory.
  • Safe. The transaction is rolled back after each test, so tests never leave data behind.
  • No setup work. You do not install or configure a database for the test.

The Entities

Our example has two entities. An author writes many books, and each book has one author.

text
AUTHOR 1 ------- * BOOK AUTHOR BOOK ------ ---- PK id PK id name title available FK author_id

This diagram shows the one-to-many link. The AUTHOR table has a primary key id and a name. The BOOK table has its own key, a title, an availability flag and a foreign key author_id that points back to the author. One author row can be linked from many book rows.

EntityFieldsNotes
Authorid, namePrimary key generated by the database
Bookid, title, available, authorMany books point to one author

How it works

When the test class starts, Spring Boot creates a small context with the JPA configuration, swaps the data source for an embedded one, and creates the tables. Every test then runs inside a transaction.

text
test starts | v begin transaction | v insert test data | v call repository method | v assert the result | v rollback (data gone)

The rollback is the key idea. Whatever a test saves is removed when it ends, so the next test starts with an empty database. This is why each test can create its own data in @BeforeEach and never worry about leftovers.

Real-Life Example

Lakeview Library keeps books and their authors. Staff often ask two questions: "Which books are on the shelf right now?" and "What has Tagore written?" Both are repository queries. A mistake in the query, such as the wrong field name, would show wrong books to the staff. With @DataJpaTest the library can put five books in a test database, run the queries, and check the answers without touching the real catalogue.

Code Example

Let's build the library shelf with two derived queries and test them against H2.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.lakeview</groupId> <artifactId>shelf</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: ShelfApplication.java in package com.lake.shelf

java
package com.lake.shelf; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ShelfApplication { public static void main(String[] args) { SpringApplication.run(ShelfApplication.class, args); } }

File: Author.java in package com.lake.shelf

java
package com.lake.shelf; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; protected Author() { } public Author(String name) { this.name = name; } public Long getId() { return id; } public String getName() { return name; } }

File: Book.java in package com.lake.shelf

java
package com.lake.shelf; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String title; private boolean available; @ManyToOne private Author author; protected Book() { } public Book(String title, boolean available, Author author) { this.title = title; this.available = available; this.author = author; } public Long getId() { return id; } public String getTitle() { return title; } public boolean isAvailable() { return available; } public Author getAuthor() { return author; } }

File: BookRepository.java in package com.lake.shelf

java
package com.lake.shelf; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface BookRepository extends JpaRepository<Book, Long> { List<Book> findByAvailableTrue(); List<Book> findByAuthorName(String name); }

File: BookRepositoryTest.java in src/test/java/com/lake/shelf

java
package com.lake.shelf; import static org.assertj.core.api.Assertions.assertThat; import java.util.List; import org.junit.jupiter.api.BeforeEach; 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; @DataJpaTest class BookRepositoryTest { @Autowired private TestEntityManager em; @Autowired private BookRepository books; @BeforeEach void setUp() { Author tagore = em.persist(new Author("Tagore")); Author narayan = em.persist(new Author("R. K. Narayan")); em.persist(new Book("Gitanjali", true, tagore)); em.persist(new Book("The Home and the World", false, tagore)); em.persist(new Book("Malgudi Days", true, narayan)); em.flush(); } @Test void findsOnlyAvailableBooks() { List<Book> result = books.findByAvailableTrue(); assertThat(result).extracting(Book::getTitle) .containsExactlyInAnyOrder("Gitanjali", "Malgudi Days"); } @Test void findsBooksByAuthorName() { List<Book> result = books.findByAuthorName("Tagore"); assertThat(result).hasSize(2); } @Test void saveGivesAnId() { Author author = em.persist(new Author("Premchand")); Book saved = books.save(new Book("Godaan", true, author)); assertThat(saved.getId()).isNotNull(); assertThat(books.count()).isEqualTo(4); } @Test void eachTestStartsClean() { assertThat(books.count()).isEqualTo(3); } }

Run the tests:

bash
./mvnw test

Output:

text
Tests run: 4 Failures: 0 Errors: 0 Skipped: 0 BUILD SUCCESS

Maven prints the four counts on one line; they are split here for reading. The console log also showed Spring starting an embedded database with an H2 URL that begins with jdbc:h2:mem, so no real database was involved.

Code Explained

  • @DataJpaTest loads the JPA slice. In Spring Boot 4 it lives in the package data.jpa.test.autoconfigure under org.springframework.boot, and TestEntityManager lives in jpa.test.autoconfigure. Both come from the data JPA test starter, which we added with test scope.
  • The H2 dependency has runtime scope. It is the embedded database that the slice uses.
  • TestEntityManager.persist saves test data directly, so the test does not depend on the repository it checks. flush sends the inserts to the database.
  • findByAvailableTrue and findByAuthorName are derived queries. Spring Data builds the SQL from the method names. AuthorName means the name field of the author property.
  • containsExactlyInAnyOrder checks the two available titles regardless of order.
  • saveGivesAnId shows that a saved book receives a generated id, and the count rises to four.
  • eachTestStartsClean proves the rollback. The count is three, exactly what setUp inserted, even though another test had saved a book.
  • The entities have a protected no-argument constructor because JPA needs one.

Real Database or H2?

By default the slice replaces your data source with an embedded database, even if the app is configured for MySQL. That keeps tests fast. But H2 is not MySQL, and some queries behave differently. When you need the real database engine, run the test against a container, which is the topic of the Testcontainers guide, and switch the replacement off with @AutoConfigureTestDatabase.

Common Mistakes

  • Forgetting the H2 dependency. With no embedded database on the classpath, the slice cannot replace the data source and fails to start.
  • Testing the repository with itself. If you save with the same repository you test, a bug can hide on both sides. Use TestEntityManager to set up data.
  • Not flushing. Without flush, changes may stay in memory and a query may not see them, which hides real SQL problems.
  • Testing Spring Data itself. save and count already work. Test your own queries and mappings.
  • Expecting data between tests. The rollback removes everything, so create the data inside each test or in @BeforeEach.

Interview Questions

What does `@DataJpaTest` load?

Ans:Only the JPA layer: entities, repositories and an embedded database, plus a transaction for each test.

Why is the data gone after each test?

Ans:Every test runs in a transaction that is rolled back at the end.

How do you use a real database instead of H2 with it?

Ans:Add @AutoConfigureTestDatabase with replace = NONE and point the test at your real or container database.

Key Points to Remember

  • @DataJpaTest is a slice test for entities and repositories.
  • It uses an embedded database and rolls back after every test.
  • TestEntityManager prepares test data.
  • Derived queries are tested as real SQL.
  • H2 is quick but is not your production database.
  • Keep tests small and independent.

Frequently Asked Questions

What is @DataJpaTest used for?

It is used to test JPA entities and repository queries quickly against an in-memory database, without loading controllers or services.

Does @DataJpaTest use my real database?

No. By default it replaces your database with an embedded one such as H2.

Why does @DataJpaTest roll back my data?

To keep tests independent. Each test runs in a transaction that is undone at the end, so the next test starts clean.

Can I use @DataJpaTest with MySQL or PostgreSQL?

Yes, by turning off the database replacement and connecting to a real database, usually through a Testcontainers container.

Practice Problems

Try each problem on your own first. Both projects need JPA, H2 and the data JPA test starter, so each brings its own pom.xml.

Easy: Cheap Cakes Query

SweetCrumbs has a Cake entity with name and price. Add a repository method that finds cakes cheaper than a given price, and write a @DataJpaTest that saves three cakes (Vanilla 300, Chocolate 450, Truffle 700) and checks that the query for below 500 returns two.

Show answer
Spring Data builds the query from the method name. The test saves three rows and expects the two below 500.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.crumbs</groupId> <artifactId>cakes</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: CakesApplication.java in package com.crumbs.cakes

java
package com.crumbs.cakes; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class CakesApplication { public static void main(String[] args) { SpringApplication.run(CakesApplication.class, args); } }

File: Cake.java in package com.crumbs.cakes

java
package com.crumbs.cakes; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Cake { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int price; protected Cake() { } public Cake(String name, int price) { this.name = name; this.price = price; } public String getName() { return name; } public int getPrice() { return price; } }

File: CakeRepository.java in package com.crumbs.cakes

java
package com.crumbs.cakes; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface CakeRepository extends JpaRepository<Cake, Long> { List<Cake> findByPriceLessThan(int price); }

File: CakeRepositoryTest.java in src/test/java/com/crumbs/cakes

java
package com.crumbs.cakes; 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.jpa.test.autoconfigure.TestEntityManager; @DataJpaTest class CakeRepositoryTest { @Autowired private TestEntityManager em; @Autowired private CakeRepository cakes; @Test void findsCakesBelow500() { em.persist(new Cake("Vanilla", 300)); em.persist(new Cake("Chocolate", 450)); em.persist(new Cake("Truffle", 700)); em.flush(); assertThat(cakes.findByPriceLessThan(500)) .extracting(Cake::getName) .containsExactlyInAnyOrder("Vanilla", "Chocolate"); } }

Run ./mvnw test. Maven reports (the counts are split over lines here):

text
Tests run: 1 Failures: 0 Errors: 0 Skipped: 0 BUILD SUCCESS

Medium: Veg Dishes Sorted and Counted

TiffinBox stores Dish rows with name, price and veg. Add two repository methods: one that returns the veg dishes sorted by price from lowest to highest, and one that counts the non-veg dishes. Write tests for both, and add a third test that shows the database is empty at the start of each test.

Show answer
The method names describe the filter and the order. Because each test rolls back, the third test starts with no rows.

File: pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.tiffin</groupId> <artifactId>box</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

File: BoxApplication.java in package com.tiffin.box

java
package com.tiffin.box; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class BoxApplication { public static void main(String[] args) { SpringApplication.run(BoxApplication.class, args); } }

File: Dish.java in package com.tiffin.box

java
package com.tiffin.box; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Dish { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private int price; private boolean veg; protected Dish() { } public Dish(String name, int price, boolean veg) { this.name = name; this.price = price; this.veg = veg; } public String getName() { return name; } public int getPrice() { return price; } }

File: DishRepository.java in package com.tiffin.box

java
package com.tiffin.box; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface DishRepository extends JpaRepository<Dish, Long> { List<Dish> findByVegTrueOrderByPriceAsc(); long countByVegFalse(); }

File: DishRepositoryTest.java in src/test/java/com/tiffin/box

java
package com.tiffin.box; 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.jpa.test.autoconfigure.TestEntityManager; @DataJpaTest class DishRepositoryTest { @Autowired private TestEntityManager em; @Autowired private DishRepository dishes; private void addDishes() { em.persist(new Dish("Paneer Butter Masala", 220, true)); em.persist(new Dish("Chicken Biryani", 260, false)); em.persist(new Dish("Masala Dosa", 120, true)); em.flush(); } @Test void vegDishesAreSortedByPrice() { addDishes(); assertThat(dishes.findByVegTrueOrderByPriceAsc()) .extracting(Dish::getName) .containsExactly("Masala Dosa", "Paneer Butter Masala"); } @Test void countsNonVegDishes() { addDishes(); assertThat(dishes.countByVegFalse()).isEqualTo(1); } @Test void startsWithAnEmptyTable() { assertThat(dishes.count()).isZero(); } }

Run ./mvnw test. Maven reports (the counts are split over lines here):

text
Tests run: 3 Failures: 0 Errors: 0 Skipped: 0 BUILD SUCCESS