Introduction
Unit testing ensures that individual parts of your application work correctly. In this tutorial, we will walk through how to unit test the repository layer in a Spring Boot application using JUnit 5 and @DataJpaTest
.
Why Should You Test the Repository Layer?
Spring Data JPA gives you ready-made methods like save()
, findById()
, and delete()
, which are already well-tested. But there are good reasons to write tests for your repository:
- Custom Queries: You may create specific queries using JPQL or SQL, which need to be tested.
- Database Interaction: You want to ensure that the repository interacts with the database as expected, even for edge cases.
- Code Refactoring: Tests make sure that future changes won’t accidentally break your data access code.
Tools and Technologies Used:
- Spring Boot 3 or later
- Java 21 or later
- H2 Database
- Spring Data JPA
- Lombok
- Spring Boot Testing
Why Use @DataJpaTest?
The @DataJpaTest
annotation is used specifically to test the JPA repositories. It sets up an in-memory database, configures Spring Data JPA, and scans the repository layer.
- It is optimized for repository testing by disabling full auto-configuration and only loading beans required for repository layer tests.
- It uses transactional rollback by default, which means that any changes made during the test will be rolled back after the test.
Why Use the H2 Database for Unit Testing?
The H2 database is an in-memory database that allows you to run tests without requiring an external database server. It is ideal for unit testing because:
- It is lightweight and fast, which speeds up testing.
- It provides the same behavior as production databases, ensuring that the repository behaves correctly.
- No persistent data is stored, and everything is cleared after each test.
Add Maven Dependencies:
To enable repository testing, add the following dependencies to your pom.xml:
<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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-data-jpa
: Includes JPA and Hibernate to handle database operations.h2
: Provides an in-memory database used for testing purposes.lombok
: Auto-generates boilerplate code like getters, setters, constructors, etc.spring-boot-starter-test
: Provides libraries like JUnit for unit testing.
Create Entity - Employee JPA Entity
Now, we define an Employee
JPA entity, which will map to a database table named employees
.
import lombok.*;
import jakarta.persistence.*;
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "first_name", nullable = false)
private String firstName;
@Column(name = "last_name", nullable = false)
private String lastName;
@Column(nullable = false)
private String email;
}
Explanation:
@Entity
: Marks this class as a JPA entity that will map to a table in the database.@Table(name = "employees")
: Specifies the name of the database table for the entity.@Id
and@GeneratedValue
: Defines the primary key (id
) and its auto-generation strategy.@Column(nullable = false)
: Specifies the database columns and constraints.- Lombok Annotations (
@Setter
,@Getter
,@Builder
,@AllArgsConstructor
,@NoArgsConstructor
): These generate the constructor, getter, and setter methods for the entity, reducing boilerplate code.
Create Repository Layer - EmployeeRepository
Next, we create the EmployeeRepository
interface, which extends the JpaRepository
. This provides default CRUD operations. Additionally, we define custom queries using JPQL and native SQL.
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByEmail(String email);
// Custom JPQL query with index parameters
@Query("select e from Employee e where e.firstName = ?1 and e.lastName = ?2")
Employee findByJPQL(String firstName, String lastName);
// Custom JPQL query with named parameters
@Query("select e from Employee e where e.firstName =:firstName and e.lastName =:lastName")
Employee findByJPQLNamedParams(@Param("firstName") String firstName, @Param("lastName") String lastName);
// Custom native SQL query with index parameters
@Query(value = "select * from employees e where e.first_name = ?1 and e.last_name = ?2", nativeQuery = true)
Employee findByNativeSQL(String firstName, String lastName);
// Custom native SQL query with named parameters
@Query(value = "select * from employees e where e.first_name =:firstName and e.last_name =:lastName", nativeQuery = true)
Employee findByNativeSQLNamed(@Param("firstName") String firstName, @Param("lastName") String lastName);
}
Explanation of Repository Methods:
findByEmail(String email)
: Finds an employee by their email address.findByJPQL(String firstName, String lastName)
: A custom JPQL query using indexed parameters to find an employee based on first and last name.findByJPQLNamedParams(String firstName, String lastName)
: A custom JPQL query using named parameters for readability.findByNativeSQL(String firstName, String lastName)
: A custom native SQL query using indexed parameters.findByNativeSQLNamed(String firstName, String lastName)
: A custom native SQL query using named parameters.
Testing Repository - EmployeeRepositoryTests
Now, we’ll write tests for each of these repository methods. These tests use @DataJpaTest
, which automatically configures an H2 in-memory database and scans for JPA repositories.
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
import java.util.Optional;
@DataJpaTest
public class EmployeeRepositoryTests {
@Autowired
private EmployeeRepository employeeRepository;
private Employee employee;
@BeforeEach
public void setup(){
employee = Employee.builder()
.firstName("Ramesh")
.lastName("Fadatare")
.email("ramesh@gmail.com")
.build();
}
// JUnit test for save employee operation
@Test
public void givenEmployeeObject_whenSave_thenReturnSavedEmployee(){
// given - precondition or setup
Employee employee = Employee.builder()
.firstName("Ramesh")
.lastName("Ramesh")
.email("ramesh@gmail.com")
.build();
// when - action or the behaviour that we are going test
Employee savedEmployee = employeeRepository.save(employee);
// then - verify the output
assertThat(savedEmployee).isNotNull();
assertThat(savedEmployee.getId()).isGreaterThan(0);
}
// JUnit test for get all employees operation
@DisplayName("JUnit test for get all employees operation")
@Test
public void givenEmployeesList_whenFindAll_thenEmployeesList(){
// given - precondition or setup
Employee employee1 = Employee.builder()
.firstName("John")
.lastName("Cena")
.email("cena@gmail.com")
.build();
employeeRepository.save(employee);
employeeRepository.save(employee1);
// when - action or the behaviour that we are going test
List<Employee> employeeList = employeeRepository.findAll();
// then - verify the output
assertThat(employeeList).isNotNull();
assertThat(employeeList.size()).isEqualTo(2);
}
// JUnit test for get employee by id operation
@DisplayName("JUnit test for get employee by id operation")
@Test
public void givenEmployeeObject_whenFindById_thenReturnEmployeeObject(){
// given - precondition or setup
employeeRepository.save(employee);
// when - action or the behaviour that we are going test
Employee employeeDB = employeeRepository.findById(employee.getId()).get();
// then - verify the output
assertThat(employeeDB).isNotNull();
}
// JUnit test for get employee by email operation
@DisplayName("JUnit test for get employee by email operation")
@Test
public void givenEmployeeEmail_whenFindByEmail_thenReturnEmployeeObject(){
// given - precondition or setup
employeeRepository.save(employee);
// when - action or the
behaviour that we are going test
Employee employeeDB = employeeRepository.findByEmail(employee.getEmail()).get();
// then - verify the output
assertThat(employeeDB).isNotNull();
}
// JUnit test for update employee operation
@DisplayName("JUnit test for update employee operation")
@Test
public void givenEmployeeObject_whenUpdateEmployee_thenReturnUpdatedEmployee(){
// given - precondition or setup
employeeRepository.save(employee);
// when - action or the behaviour that we are going test
Employee savedEmployee = employeeRepository.findById(employee.getId()).get();
savedEmployee.setEmail("ram@gmail.com");
savedEmployee.setFirstName("Ram");
Employee updatedEmployee = employeeRepository.save(savedEmployee);
// then - verify the output
assertThat(updatedEmployee.getEmail()).isEqualTo("ram@gmail.com");
assertThat(updatedEmployee.getFirstName()).isEqualTo("Ram");
}
// JUnit test for delete employee operation
@DisplayName("JUnit test for delete employee operation")
@Test
public void givenEmployeeObject_whenDelete_thenRemoveEmployee(){
// given - precondition or setup
employeeRepository.save(employee);
// when - action or the behaviour that we are going test
employeeRepository.deleteById(employee.getId());
Optional<Employee> employeeOptional = employeeRepository.findById(employee.getId());
// then - verify the output
assertThat(employeeOptional).isEmpty();
}
// JUnit test for custom query using JPQL with index
@DisplayName("JUnit test for custom query using JPQL with index")
@Test
public void givenFirstNameAndLastName_whenFindByJPQL_thenReturnEmployeeObject(){
// given - precondition or setup
employeeRepository.save(employee);
String firstName = "Ramesh";
String lastName = "Fadatare";
// when - action or the behaviour that we are going test
Employee savedEmployee = employeeRepository.findByJPQL(firstName, lastName);
// then - verify the output
assertThat(savedEmployee).isNotNull();
}
// JUnit test for custom query using JPQL with Named params
@DisplayName("JUnit test for custom query using JPQL with Named params")
@Test
public void givenFirstNameAndLastName_whenFindByJPQLNamedParams_thenReturnEmployeeObject(){
// given - precondition or setup
employeeRepository.save(employee);
String firstName = "Ramesh";
String lastName = "Fadatare";
// when - action or the behaviour that we are going test
Employee savedEmployee = employeeRepository.findByJPQLNamedParams(firstName, lastName);
// then - verify the output
assertThat(savedEmployee).isNotNull();
}
// JUnit test for custom query using native SQL with index
@DisplayName("JUnit test for custom query using native SQL with index")
@Test
public void givenFirstNameAndLastName_whenFindByNativeSQL_thenReturnEmployeeObject(){
// given - precondition or setup
employeeRepository.save(employee);
// when - action or the behaviour that we are going test
Employee savedEmployee = employeeRepository.findByNativeSQL(employee.getFirstName(), employee.getLastName());
// then - verify the output
assertThat(savedEmployee).isNotNull();
}
// JUnit test for custom query using native SQL with named params
@DisplayName("JUnit test for custom query using native SQL with named params")
@Test
public void givenFirstNameAndLastName_whenFindByNativeSQLNamedParams_thenReturnEmployeeObject(){
// given - precondition or setup
employeeRepository.save(employee);
// when - action or the behaviour that we are going test
Employee savedEmployee = employeeRepository.findByNativeSQLNamed(employee.getFirstName(), employee.getLastName());
// then - verify the output
assertThat(savedEmployee).isNotNull();
}
}
Explanation of JUnit Test Methods:
givenEmployeeObject_whenSave_thenReturnSavedEmployee()
: Tests if anEmployee
object is saved correctly using thesave()
method.givenEmployeesList_whenFindAll_thenEmployeesList()
: Verifies that thefindAll()
method returns a list of employees.givenEmployeeObject_whenFindById_thenReturnEmployeeObject()
: Tests if the repository can retrieve an employee byid
usingfindById()
.givenEmployeeEmail_whenFindByEmail_thenReturnEmployeeObject()
: Verifies that the repository can retrieve an employee byemail
usingfindByEmail()
.givenEmployeeObject_whenUpdateEmployee_thenReturnUpdatedEmployee()
: Tests if anEmployee
object is updated correctly.givenEmployeeObject_whenDelete_thenRemoveEmployee()
: Tests if anEmployee
object is deleted from the database.- Custom Query Tests (
findByJPQL()
,findByJPQLNamedParams()
, etc.): These tests validate that custom JPQL and native SQL queries return the correct employee records.
Conclusion
In this tutorial, we walked through how to write unit tests for the repository layer in a Spring Boot application using JUnit 5 and @DataJpaTest
. We covered various repository methods, including basic CRUD operations and custom queries, and explained how using the H2 database allows us to efficiently test these interactions in an in-memory environment.
Comments
Post a Comment
Leave Comment