- Set up repository tests using
@DataJpaTestand understand what it auto-configures - Write tests for derived query methods and custom
@Querymethods - Use
TestEntityManagerto set up test data without going through the repository under test - Test projection interfaces and DTO queries
Your repository method looks correct. The query compiles. But does it actually return what you expect when the data has edge cases? An employee with a null department. A salary search that should return nothing. A projection that might silently drop a column. You will not know until you test it.
Spring Boot makes repository testing straightforward with @DataJpaTest. It spins up an in-memory database, configures JPA, and rolls back each test automatically. No need to clean up test data manually. No interference between tests. The entire slice of your application that deals with persistence is available, but nothing else, so the tests run fast.
We will test an Employee repository throughout this post. The entity has five fields, and the repository has a mix of derived queries and custom JPQL.
package academy.javapro;
import jakarta.persistence.*;
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String department;
private Double salary;
public Employee() {}
public Employee(String name, String email, String department, Double salary) {
this.name = name;
this.email = email;
this.department = department;
this.salary = salary;
}
public Long getId() { return id; }
public String getName() { return name; }
public String getEmail() { return email; }
public String getDepartment() { return department; }
public Double getSalary() { return salary; }
}The constructor without an ID is useful for tests. JPA generates the ID on persist, so test code can create employees without worrying about ID values.
package academy.javapro;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
List<Employee> findByDepartment(String department);
List<Employee> findBySalaryGreaterThan(Double minSalary);
@Query("SELECT e FROM Employee e WHERE e.department = :dept ORDER BY e.salary DESC")
List<Employee> findByDepartmentOrderedBySalary(String dept);
}Three methods to test: a simple derived query by department, a comparison query for salary, and a custom JPQL query with ordering.
The @DataJpaTest annotation configures an embedded database, scans for @Entity classes, and configures Spring Data JPA repositories. It does not load @Service, @Controller, or other components. Each test runs in a transaction that rolls back after completion.
package academy.javapro;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class EmployeeRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
}TestEntityManager is a wrapper around JPA's EntityManager designed for tests. Use it to insert test data directly, bypassing the repository you are testing. This avoids circular logic where the test relies on the same code it is supposed to verify.
The first test checks findByDepartment(). We insert two employees in Engineering and one in Sales, then verify the query returns only the Engineering employees.
@Test
void findByDepartment_returnsMatchingEmployees() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 90000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 85000.0));
entityManager.persist(new Employee("Carol", "carol@test.com", "Sales", 75000.0));
entityManager.flush();
List<Employee> engineers = employeeRepository.findByDepartment("Engineering");
assertThat(engineers).hasSize(2);
assertThat(engineers).extracting(Employee::getName).containsExactlyInAnyOrder("Alice", "Bob");
}The flush() call forces Hibernate to write the inserts to the database immediately. Without it, the entities might stay in the persistence context without hitting the actual table, and your query could behave differently than in production.
AssertJ's extracting() method pulls the name field from each employee, making the assertion readable. The test verifies both the count and the actual names returned.
Queries should return empty collections, not null, when nothing matches. This test verifies that behavior explicitly.
@Test
void findByDepartment_returnsEmptyListWhenNoMatch() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 90000.0));
entityManager.flush();
List<Employee> marketing = employeeRepository.findByDepartment("Marketing");
assertThat(marketing).isEmpty();
}A simple test, but it catches a class of bugs where developers return null instead of an empty list, or where a query has a typo that never matches anything.
The findBySalaryGreaterThan() method needs boundary testing. What happens at exactly the threshold?
@Test
void findBySalaryGreaterThan_excludesBoundaryValue() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 80000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 80001.0));
entityManager.persist(new Employee("Carol", "carol@test.com", "Sales", 79999.0));
entityManager.flush();
List<Employee> highEarners = employeeRepository.findBySalaryGreaterThan(80000.0);
assertThat(highEarners).hasSize(1);
assertThat(highEarners.get(0).getName()).isEqualTo("Bob");
}Alice earns exactly 80,000, which is not greater than 80,000. Only Bob at 80,001 qualifies. This test documents the boundary behavior so future developers know the method excludes the exact threshold.
The @Query method findByDepartmentOrderedBySalary() filters by department and orders by salary descending. The test verifies both the filtering and the ordering.
@Test
void findByDepartmentOrderedBySalary_returnsSortedResults() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 70000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 90000.0));
entityManager.persist(new Employee("Carol", "carol@test.com", "Engineering", 80000.0));
entityManager.persist(new Employee("Dave", "dave@test.com", "Sales", 95000.0));
entityManager.flush();
List<Employee> engineers = employeeRepository.findByDepartmentOrderedBySalary("Engineering");
assertThat(engineers).hasSize(3);
assertThat(engineers).extracting(Employee::getName)
.containsExactly("Bob", "Carol", "Alice");
}The containsExactly() assertion checks order. Bob has the highest salary, then Carol, then Alice. Dave is in Sales and should not appear at all. If someone accidentally removes the ORDER BY clause from the JPQL, this test fails.
If your repository returns projections, test those too. Add a projection interface and a repository method:
package academy.javapro;
public interface EmployeeNameView {
Long getId();
String getName();
}// Add to EmployeeRepository
List<EmployeeNameView> findEmployeeNamesByDepartment(String department);The test verifies that the projection returns only the expected fields and maps them correctly:
@Test
void findEmployeeNamesByDepartment_returnsProjection() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 90000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 85000.0));
entityManager.flush();
List<EmployeeNameView> names = employeeRepository.findEmployeeNamesByDepartment("Engineering");
assertThat(names).hasSize(2);
assertThat(names).extracting(EmployeeNameView::getName)
.containsExactlyInAnyOrder("Alice", "Bob");
}The test treats the projection as a black box. It does not care how Spring Data JPA implements the interface; it only verifies the returned data is correct.
Here is the full test class with all tests in one place:
package academy.javapro;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class EmployeeRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
@Test
void findByDepartment_returnsMatchingEmployees() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 90000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 85000.0));
entityManager.persist(new Employee("Carol", "carol@test.com", "Sales", 75000.0));
entityManager.flush();
List<Employee> engineers = employeeRepository.findByDepartment("Engineering");
assertThat(engineers).hasSize(2);
assertThat(engineers).extracting(Employee::getName)
.containsExactlyInAnyOrder("Alice", "Bob");
}
@Test
void findByDepartment_returnsEmptyListWhenNoMatch() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 90000.0));
entityManager.flush();
List<Employee> marketing = employeeRepository.findByDepartment("Marketing");
assertThat(marketing).isEmpty();
}
@Test
void findBySalaryGreaterThan_excludesBoundaryValue() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 80000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 80001.0));
entityManager.persist(new Employee("Carol", "carol@test.com", "Sales", 79999.0));
entityManager.flush();
List<Employee> highEarners = employeeRepository.findBySalaryGreaterThan(80000.0);
assertThat(highEarners).hasSize(1);
assertThat(highEarners.get(0).getName()).isEqualTo("Bob");
}
@Test
void findByDepartmentOrderedBySalary_returnsSortedResults() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 70000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 90000.0));
entityManager.persist(new Employee("Carol", "carol@test.com", "Engineering", 80000.0));
entityManager.persist(new Employee("Dave", "dave@test.com", "Sales", 95000.0));
entityManager.flush();
List<Employee> engineers = employeeRepository.findByDepartmentOrderedBySalary("Engineering");
assertThat(engineers).hasSize(3);
assertThat(engineers).extracting(Employee::getName)
.containsExactly("Bob", "Carol", "Alice");
}
@Test
void findEmployeeNamesByDepartment_returnsProjection() {
entityManager.persist(new Employee("Alice", "alice@test.com", "Engineering", 90000.0));
entityManager.persist(new Employee("Bob", "bob@test.com", "Engineering", 85000.0));
entityManager.flush();
List<EmployeeNameView> names = employeeRepository.findEmployeeNamesByDepartment("Engineering");
assertThat(names).hasSize(2);
assertThat(names).extracting(EmployeeNameView::getName)
.containsExactlyInAnyOrder("Alice", "Bob");
}
}You might wonder why we use TestEntityManager to insert data instead of employeeRepository.save(). The reason is test isolation. If save() has a bug, tests that rely on it for setup will fail for the wrong reason. By using TestEntityManager, the setup is independent of the code under test.
There is also a clarity benefit. When you see entityManager.persist() in a test, you know it is setup. When you see employeeRepository.findByDepartment(), you know it is the method being tested. The distinction is immediate.
@DataJpaTest creates a minimal Spring context with JPA and an embedded database. Tests run in transactions that roll back automatically, keeping each test isolated.
Use TestEntityManager to insert test data instead of the repository you are testing. This keeps setup independent and makes test failures easier to diagnose.
Test boundary conditions explicitly. A salary query that uses "greater than" should have a test proving the exact boundary is excluded. Document edge cases through tests so future developers know the expected behavior.
Projections deserve their own tests. Verify that the interface returns the correct fields and maps them properly. If someone changes the projection interface, the test should catch mismatches immediately.
Testing Spring Data JPA Repositories. Last updated February 9, 2026.
Ready to level up your Java skills? Start with our free Core Java course, explore more topics like Testing Spring Data JPA Repositories on the blog, or join the full Java Bootcamp to master enterprise-level development.