Introduction
In this tutorial, we will focus on unit testing the service layer of a Spring Boot application using JUnit 5 and Mockito. Unit 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.
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
│ └── rameshfadatare
│ └── springboot
│ ├── model
│ │ └── Employee.java
│ ├── repository
│ │ └── EmployeeRepository.java
│ └── service
│ └── EmployeeService.java
│ └── impl
│ └── EmployeeServiceImpl.java
│
src/test/java
│
├── net
│ └── rameshfadatare
│ └── springboot
│ └── service
│ └── EmployeeServiceTests.java
Step 1: Create the Employee
Entity
The Employee
entity will map to the employees
table in the database:
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 asemployees
.- 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.
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
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:
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:
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.
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:
- The
given()
method is used to mock thefindByEmail()
method of the repository to return an emptyOptional
object, simulating that no employee with the same email exists. - The
saveEmployee()
method is called to test the service’s ability to save the employee object. - The
assertThat(savedEmployee).isNotNull()
statement verifies that the employee has been successfully saved and the returned object is notnull
.
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:
- The
given()
method is used to simulate that an employee with the same email already exists in the repository. - The test uses
assertThrows()
to verify that aResourceNotFoundException
is thrown when trying to save an employee with an existing email. - The
verify()
method ensures that thesave()
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:
- The
given()
method is used to mock thefindAll()
method of the repository, simulating that two employees are already present in the repository. - The
getAllEmployees()
method is called to test the service’s functionality to retrieve all employees. assertThat(employeeList).isNotNull()
checks that the list is not empty, andassertThat(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:
- The
given()
method is used to mock thefindAll()
method of the repository to return an empty list. - The
getAllEmployees()
method is tested to ensure it correctly handles the scenario where no employees exist. - The
assertThat(employeeList).isEmpty()
method checks that the returned list is empty, andassertThat(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:
- The
given()
method is used to simulate the scenario where an employee with ID1L
exists in the repository. - The
getEmployeeById()
method is called to retrieve the employee by their ID. - 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:
- The
given()
method mocks thesave()
method of the repository to return the updated employee object. - The
updateEmployee()
method is tested to ensure the employee’s details can be successfully updated. - The
assertThat(updatedEmployee.getEmail()).isEqualTo("ram@gmail.com")
checks that the email has been updated correctly, andassertThat(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:
- The
willDoNothing()
method mocks thedeleteById()
method to simulate the deletion of an employee. - The
deleteEmployee()
method is called to delete the employee with ID1L
. - The
verify()
method checks thatdeleteById()
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.