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.
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
findByAuthorNameis 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.
textAUTHOR 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.
| Entity | Fields | Notes |
|---|---|---|
Author | id, name | Primary key generated by the database |
Book | id, title, available, author | Many 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.
texttest 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
javapackage 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
javapackage 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
javapackage 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
javapackage 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
javapackage 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:
textTests 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
@DataJpaTestloads the JPA slice. In Spring Boot 4 it lives in the packagedata.jpa.test.autoconfigureunderorg.springframework.boot, andTestEntityManagerlives injpa.test.autoconfigure. Both come from the data JPA test starter, which we added withtestscope.- The H2 dependency has
runtimescope. It is the embedded database that the slice uses. TestEntityManager.persistsaves test data directly, so the test does not depend on the repository it checks.flushsends the inserts to the database.findByAvailableTrueandfindByAuthorNameare derived queries. Spring Data builds the SQL from the method names.AuthorNamemeans thenamefield of theauthorproperty.containsExactlyInAnyOrderchecks the two available titles regardless of order.saveGivesAnIdshows that a saved book receives a generated id, and the count rises to four.eachTestStartsCleanproves the rollback. The count is three, exactly whatsetUpinserted, 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
TestEntityManagerto 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.
saveandcountalready 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
@DataJpaTestis a slice test for entities and repositories.- It uses an embedded database and rolls back after every test.
TestEntityManagerprepares 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.
Related Topics
- Testcontainers Basics: test against a real database in a container.
- JpaRepository: the interface that gives you the queries under test.
- Derived Query Methods: how method names become SQL.
- H2 In-Memory Database: the embedded database used here.
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 answerHide answer
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
javapackage 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
javapackage 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
javapackage 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
javapackage 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):
textTests 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 answerHide answer
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
javapackage 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
javapackage 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
javapackage 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
javapackage 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):
textTests run: 3 Failures: 0 Errors: 0 Skipped: 0 BUILD SUCCESS