Unit Testing Spring Boot Repository Layer

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.

Unit Testing Spring Boot Repository Layer

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:

  1. Custom Queries: You may create specific queries using JPQL or SQL, which need to be tested.
  2. Database Interaction: You want to ensure that the repository interacts with the database as expected, even for edge cases.
  3. 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 an Employee object is saved correctly using the save() method.
  • givenEmployeesList_whenFindAll_thenEmployeesList(): Verifies that the findAll() method returns a list of employees.
  • givenEmployeeObject_whenFindById_thenReturnEmployeeObject(): Tests if the repository can retrieve an employee by id using findById().
  • givenEmployeeEmail_whenFindByEmail_thenReturnEmployeeObject(): Verifies that the repository can retrieve an employee by email using findByEmail().
  • givenEmployeeObject_whenUpdateEmployee_thenReturnUpdatedEmployee(): Tests if an Employee object is updated correctly.
  • givenEmployeeObject_whenDelete_thenRemoveEmployee(): Tests if an Employee 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