Unit Testing Spring Boot Service Layer

Introduction

In this tutorial, we will focus on unit testing the service layer of a Spring Boot application using JUnit 5 and MockitoUnit testing is the process of testing individual units or components of an application in isolation. This ensures that each unit behaves correctly in different scenarios.

Unit Testing Spring Boot Service Layer

JUnit 5 is a popular testing framework for Java applications. It provides rich support for writing and running automated tests.

Mockito is a mocking framework that allows you to create mock objects that simulate the behaviour of real dependencies during unit testing.

Tools and Technologies Used

  • Java 21+
  • Spring Boot
  • Lombok
  • JUnit 5
  • Mockito
  • AssertJ
  • Maven

Add Maven Dependencies

Here are the required dependencies for Spring Boot, Lombok, JUnit 5, and Mockito. Add them to your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>9.0.0</version>
</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>

Project Structure

Here is the project structure you should follow:

src/main/java
│
├── net
│   └── javaguides
│       └── springboot
│           ├── model
│           │   └── Employee.java
│           ├── repository
│           │   └── EmployeeRepository.java
│           └── service
│               └── EmployeeService.java
│               └── impl
│                   └── EmployeeServiceImpl.java
│
src/test/java
│
├── net
│   └── javaguides
│       └── springboot
│           └── service
│               └── EmployeeServiceTests.java

Step 1: Create the Employee Entity

The Employee entity will map to the employees table in the database:

package net.javaguides.springboot.model;

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: Specifies that this class is a JPA entity.
  • @Table: Specifies the table name as employees.
  • Lombok Annotations: @Setter, @Getter, @AllArgsConstructor, @NoArgsConstructor, and @Builder auto-generate the necessary boilerplate code.

Step 2: Create the Repository Layer

The repository will provide CRUD operations and custom queries for Employee entities.

package net.javaguides.springboot.repository;

import net.javaguides.springboot.model.Employee;
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);

    @Query("select e from Employee e where e.firstName = ?1 and e.lastName = ?2")
    Employee findByJPQL(String firstName, String lastName);

    @Query("select e from Employee e where e.firstName =:firstName and e.lastName =:lastName")
    Employee findByJPQLNamedParams(@Param("firstName") String firstName, @Param("lastName") String lastName);

    @Query(value = "select * from employees e where e.first_name =?1 and e.last_name =?2", nativeQuery = true)
    Employee findByNativeSQL(String firstName, String lastName);

    @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:

  • findByEmail: Retrieves an employee based on the email.
  • findByJPQL: A custom JPQL query using index parameters.
  • findByNativeSQL: Custom native SQL query using index parameters.

Step 3: Define Custom Exception ResourceNotFoundException

package net.javaguides.springboot.exception;

public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

Step 4: Create the Service Layer

EmployeeService Interface:

package net.javaguides.springboot.service;

import net.javaguides.springboot.model.Employee;

import java.util.List;
import java.util.Optional;

public interface EmployeeService {
    Employee saveEmployee(Employee employee);
    List<Employee> getAllEmployees();
    Optional<Employee> getEmployeeById(long id);
    Employee updateEmployee(Employee updatedEmployee);
    void deleteEmployee(long id);
}

EmployeeServiceImpl Class:

package net.javaguides.springboot.service.impl;

import net.javaguides.springboot.exception.ResourceNotFoundException;
import net.javaguides.springboot.model.Employee;
import net.javaguides.springboot.repository.EmployeeRepository;
import net.javaguides.springboot.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class EmployeeServiceImpl implements EmployeeService {

    private final EmployeeRepository employeeRepository;

    @Autowired
    public EmployeeServiceImpl(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Override
    public Employee saveEmployee(Employee employee) {
        Optional<Employee> savedEmployee = employeeRepository.findByEmail(employee.getEmail());
        if (savedEmployee.isPresent()) {
            throw new ResourceNotFoundException("Employee already exists with given email: " + employee.getEmail());
        }
        return employeeRepository.save(employee);
    }

    @Override
    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Override
    public Optional<Employee> getEmployeeById(long id) {
        return employeeRepository.findById(id);
    }

    @Override
    public Employee updateEmployee(Employee updatedEmployee) {
        return employeeRepository.save(updatedEmployee);
    }

    @Override
    public void deleteEmployee(long id) {
        employeeRepository.deleteById(id);
    }
}

Step 5: Testing the Service Layer

We use @Mock annotation to create a mock instance of the EmployeeRepository. Next, we inject repository into the service using the @InjectMocks annotation.This allows the service layer to interact with the repository layer via the mock, without actually calling the real database.

The following code demonstrates unit testing for the service layer using JUnit 5 and Mockito. It follows the Given-When-Then pattern for each test case.

package net.javaguides.springboot.service;

import net.javaguides.springboot.exception.ResourceNotFoundException;
import net.javaguides.springboot.model.Employee;
import net.javaguides.springboot.repository.EmployeeRepository;
import net.javaguides.springboot.service.impl.EmployeeServiceImpl;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.Mockito.*;

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTests {

    @Mock
    private EmployeeRepository employeeRepository;

    @InjectMocks
    private EmployeeServiceImpl employeeService;

    private Employee employee;

    @BeforeEach
    public void setup(){
        employee = Employee.builder()
                .id(1L)
                .firstName("Ramesh")
                .lastName("Fadatare")
                .email("ramesh@gmail.com")
                .build();
    }

    // JUnit test for saveEmployee method
    @DisplayName("JUnit test for saveEmployee method")
    @Test
    public void givenEmployeeObject_whenSaveEmployee_thenReturnEmployeeObject(){
        //  given
        given(employeeRepository.findByEmail(employee.getEmail()))
                .willReturn(Optional.empty());

        given(employeeRepository.save(employee)).willReturn(employee);

        // when
        Employee savedEmployee = employeeService.saveEmployee(employee);

        // then
        assertThat(savedEmployee).isNotNull();
    }

    // JUnit test for saveEmployee method with exception
    @DisplayName("JUnit test for saveEmployee method which throws exception")
    @Test
    public void givenExistingEmail_whenSaveEmployee_thenThrowsException(){
        // given
        given(employeeRepository.findByEmail(employee.getEmail()))
                .willReturn(Optional.of(employee));

        // when
        org.junit.jupiter.api.Assertions.assertThrows(ResourceNotFoundException.class, () -> {
            employeeService.saveEmployee(employee);
        });

        // then
        verify(employeeRepository, never()).save(any(Employee.class));
    }

    // JUnit test for getAllEmployees method
    @DisplayName("JUnit test for getAllEmployees method")
    @Test
    public void givenEmployeesList_whenGetAllEmployees_thenReturnEmployeesList(){
        // given
        Employee employee1 = Employee.builder()
                .id(2L)
                .firstName("Tony")
                .lastName("Stark")
                .email("tony@gmail.com")
                .build();

        given(employeeRepository.findAll()).willReturn(List.of(employee, employee1));

        // when
        List<Employee> employeeList = employeeService.getAllEmployees();

        // then
        assertThat(employeeList).isNotNull();
        assertThat(employeeList.size()).isEqualTo(2);
    }

    // JUnit test for getAllEmployees method (negative scenario)
    @DisplayName("JUnit test for getAllEmployees method (negative scenario)")
    @Test
    public void givenEmptyEmployeesList_whenGetAllEmployees_thenReturnEmptyEmployeesList(){
        // given
        given(employeeRepository.findAll()).willReturn(Collections.emptyList());

        // when
        List<Employee> employeeList = employeeService.getAllEmployees();

        // then
        assertThat(employeeList).isEmpty();
        assertThat(employeeList.size()).isEqualTo(0);
    }

    // JUnit test for getEmployeeById method
    @DisplayName("JUnit test for getEmployeeById method")
    @Test
    public void givenEmployeeId_whenGetEmployeeById_thenReturnEmployeeObject(){
        // given
        given(employeeRepository.findById(1L)).willReturn(Optional.of(employee));

        // when
        Employee savedEmployee = employeeService.getEmployeeById(employee.getId()).get();

        // then
        assertThat(savedEmployee).isNotNull();
    }

    // JUnit test for updateEmployee method
    @DisplayName("JUnit test for updateEmployee method")
    @Test
    public void givenEmployeeObject_whenUpdateEmployee_thenReturnUpdatedEmployee(){
        // given
        given(employeeRepository.save(employee)).willReturn(employee);
        employee.setEmail("ram@gmail.com");
        employee.setFirstName("Ram");

        // when
        Employee updatedEmployee = employeeService.updateEmployee(employee);

        // then
        assertThat(updatedEmployee.getEmail()).isEqualTo("ram@gmail.com");
        assertThat(updatedEmployee.getFirstName()).isEqualTo("Ram");
    }

    // JUnit test for deleteEmployee method
    @DisplayName("JUnit test for deleteEmployee method")
    @Test
    public void givenEmployeeId_whenDeleteEmployee_thenNothing(){
        // given
        long employeeId = 1L;
        willDoNothing().given(employeeRepository).deleteById(employeeId);

        // when
        employeeService.deleteEmployee(employeeId);

        // then
        verify(employeeRepository, times(1)).deleteById(employeeId);
    }
}

Step 6: Explanation for Each Unit Tests

JUnit test for saveEmployee method

// JUnit test for saveEmployee method
@DisplayName("JUnit test for saveEmployee method")
@Test
public void givenEmployeeObject_whenSaveEmployee_thenReturnEmployeeObject(){
    // given - precondition or setup
    given(employeeRepository.findByEmail(employee.getEmail()))
            .willReturn(Optional.empty());

    given(employeeRepository.save(employee)).willReturn(employee);

    // when -  action or the behaviour that we are going test
    Employee savedEmployee = employeeService.saveEmployee(employee);

    // then - verify the output
    assertThat(savedEmployee).isNotNull();
}

Explanation:

  1. The given() method is used to mock the findByEmail() method of the repository to return an empty Optional object, simulating that no employee with the same email exists.
  2. The saveEmployee() method is called to test the service's ability to save the employee object.
  3. The assertThat(savedEmployee).isNotNull() statement verifies that the employee has been successfully saved and the returned object is not null.

JUnit test for saveEmployee method which throws exception

// JUnit test for saveEmployee method which throws exception
@DisplayName("JUnit test for saveEmployee method which throws exception")
@Test
public void givenExistingEmail_whenSaveEmployee_thenThrowsException(){
    // given - precondition or setup
    given(employeeRepository.findByEmail(employee.getEmail()))
            .willReturn(Optional.of(employee));

    // when -  action or the behaviour that we are going test
    org.junit.jupiter.api.Assertions.assertThrows(ResourceNotFoundException.class, () -> {
        employeeService.saveEmployee(employee);
    });

    // then
    verify(employeeRepository, never()).save(any(Employee.class));
}

Explanation:

  1. The given() method is used to simulate that an employee with the same email already exists in the repository.
  2. The test uses assertThrows() to verify that a ResourceNotFoundException is thrown when trying to save an employee with an existing email.
  3. The verify() method ensures that the save() method of the repository is never called when the exception is thrown.

JUnit test for getAllEmployees method

// JUnit test for getAllEmployees method
@DisplayName("JUnit test for getAllEmployees method")
@Test
public void givenEmployeesList_whenGetAllEmployees_thenReturnEmployeesList(){
    // given - precondition or setup
    Employee employee1 = Employee.builder()
            .id(2L)
            .firstName("Tony")
            .lastName("Stark")
            .email("tony@gmail.com")
            .build();

    given(employeeRepository.findAll()).willReturn(List.of(employee, employee1));

    // when -  action or the behaviour that we are going test
    List<Employee> employeeList = employeeService.getAllEmployees();

    // then - verify the output
    assertThat(employeeList).isNotNull();
    assertThat(employeeList.size()).isEqualTo(2);
}

Explanation:

  1. The given() method is used to mock the findAll() method of the repository, simulating that two employees are already present in the repository.
  2. The getAllEmployees() method is called to test the service's functionality to retrieve all employees.
  3. assertThat(employeeList).isNotNull() checks that the list is not empty, and assertThat(employeeList.size()).isEqualTo(2) verifies that the size of the returned list is as expected.

JUnit test for getAllEmployees method (negative scenario)

// JUnit test for getAllEmployees method (negative scenario)
@DisplayName("JUnit test for getAllEmployees method (negative scenario)")
@Test
public void givenEmptyEmployeesList_whenGetAllEmployees_thenReturnEmptyEmployeesList(){
    // given - precondition or setup
    given(employeeRepository.findAll()).willReturn(Collections.emptyList());

    // when -  action or the behaviour that we are going test
    List<Employee> employeeList = employeeService.getAllEmployees();

    // then - verify the output
    assertThat(employeeList).isEmpty();
    assertThat(employeeList.size()).isEqualTo(0);
}

Explanation:

  1. The given() method is used to mock the findAll() method of the repository to return an empty list.
  2. The getAllEmployees() method is tested to ensure it correctly handles the scenario where no employees exist.
  3. The assertThat(employeeList).isEmpty() method checks that the returned list is empty, and assertThat(employeeList.size()).isEqualTo(0) ensures that the list size is zero.

JUnit test for getEmployeeById method

// JUnit test for getEmployeeById method
@DisplayName("JUnit test for getEmployeeById method")
@Test
public void givenEmployeeId_whenGetEmployeeById_thenReturnEmployeeObject(){
    // given - precondition or setup
    given(employeeRepository.findById(1L)).willReturn(Optional.of(employee));

    // when -  action or the behaviour that we are going test
    Employee savedEmployee = employeeService.getEmployeeById(employee.getId()).get();

    // then - verify the output
    assertThat(savedEmployee).isNotNull();
}

Explanation:

  1. The given() method is used to simulate the scenario where an employee with ID 1L exists in the repository.
  2. The getEmployeeById() method is called to retrieve the employee by their ID.
  3. The assertThat(savedEmployee).isNotNull() statement verifies that the employee object is retrieved successfully and is not null.

JUnit test for updateEmployee method

// JUnit test for updateEmployee method
@DisplayName("JUnit test for updateEmployee method")
@Test
public void givenEmployeeObject_whenUpdateEmployee_thenReturnUpdatedEmployee(){
    // given - precondition or setup
    given(employeeRepository.save(employee)).willReturn(employee);
    employee.setEmail("ram@gmail.com");
    employee.setFirstName("Ram");

    // when -  action or the behaviour that we are going test
    Employee updatedEmployee = employeeService.updateEmployee(employee);

    // then - verify the output
    assertThat(updatedEmployee.getEmail()).isEqualTo("ram@gmail.com");
    assertThat(updatedEmployee.getFirstName()).isEqualTo("Ram");
}

Explanation:

  1. The given() method mocks the save() method of the repository to return the updated employee object.
  2. The updateEmployee() method is tested to ensure the employee's details can be successfully updated.
  3. The assertThat(updatedEmployee.getEmail()).isEqualTo("ram@gmail.com") checks that the email has been updated correctly, and assertThat(updatedEmployee.getFirstName()).isEqualTo("Ram") verifies the first name update.

JUnit test for deleteEmployee method

// JUnit test for deleteEmployee method
@DisplayName("JUnit test for deleteEmployee method")
@Test
public void givenEmployeeId_whenDeleteEmployee_thenNothing(){
    // given - precondition or setup
    long employeeId = 1L;

    willDoNothing().given(employeeRepository).deleteById(employeeId);

    // when -  action or the behaviour that we are going test
    employeeService.deleteEmployee(employeeId);

    // then - verify the output
    verify(employeeRepository, times(1)).deleteById(employeeId);
}

Explanation:

  1. The willDoNothing() method mocks the deleteById() method to simulate the deletion of an employee.
  2. The deleteEmployee() method is called to delete the employee with ID 1L.
  3. The verify() method checks that deleteById() is called exactly once, ensuring that the employee deletion was successful.

These test cases ensure that all CRUD operations in the service layer function as expected. Each method verifies the behavior of saving, retrieving, updating, and deleting employee objects, simulating interactions with the repository.

Conclusion

In this tutorial, we learned how to unit test the service layer in a Spring Boot application using JUnit 5 and Mockito. By mocking the repository layer, we were able to focus on testing the business logic in the service without interacting with the actual database. This approach makes the tests faster, more reliable, and easier to maintain.

Comments