RESTful API Testing in Java with Mockito (Service Layer)
Mockito is a popular Java library used for creating and working with mock objects in unit testing. It allows you to simulate the behavior of real objects or dependencies (such as database access, external services, or complex classes) so that you can test your code in isolation. Mockito makes it easier to write unit tests by providing methods to create mock objects, define their behavior, and verify interactions with those mock objects during testing. It’s commonly used in combination with testing frameworks like JUnit to write effective and reliable unit tests for Java applications.
I will use the initial branch of a Maven-Spring Boot project that you can access at this link, for the implementation of Mockito and JUnit5, using IntelliJ IDEA as the IDE. You can download the project and follow the step-by-step instructions in the article to add the necessary code to your project.
I would like to begin by explaining some existing code in the project. If you prefer to dive straight into the implementation, you can follow this link.
The dependency below is added to the project.
<!-- Spring Boot Starter Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
I have just the interface below for our EmployeeRepository
class.
package com.azizkale.mockitotutorial.dao;
import com.azizkale.mockitotutorial.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}
JpaRepository
is part of the Spring Data JPA framework, which simplifies the implementation of data access layers in Java applications using the Java Persistence API (JPA). Here are some reasons why we use JpaRepository
:
- CRUD Operations:
JpaRepository
provides out-of-the-box methods for common CRUD (Create, Read, Update, Delete) operations. We don't need to write boilerplate code for these operations; Spring Data JPA handles the implementation. - Query Methods: We can define query methods in the repository interface by following naming conventions. Spring Data JPA automatically translates these method names into SQL queries, simplifying the process of creating custom queries.
- Paging and Sorting:
JpaRepository
supports pagination and sorting out of the box. Developers can easily retrieve a subset of data and control the order of results. - Automatic Query Generation: Spring Data JPA can automatically generate queries based on method names, reducing the need for manual query writing. This is especially useful for standard queries like finding an entity by its ID or searching based on a specific field.
- Integration with Spring Framework:
JpaRepository
seamlessly integrates with the broader Spring ecosystem. This includes support for Spring's declarative transaction management, dependency injection, and other features. - Consistent API: By using a consistent repository pattern, we can easily switch between different data sources (relational databases, NoSQL databases, etc.) without changing the higher-level business logic.
Explanations of Optional
in the class EmployeeServiceImpl
:
@Override
public Optional<Employee> findById(int id) throws EmployeeNotFoundException {
Optional<Employee> employee = employeeRepository.findById(id);
if(employee == null) {
throw new EmployeeNotFoundException("Employee not found with id :" + id);
}
return employee;
}
The Optional
class is a container object that may or may not contain a non-null value. It is part of the java.util
package in Java and is commonly used to represent the possibility of a value being absent. In the context of our code, Optional<Employee>
is used in methods where a single employee is expected to be returned, such as in the findById
method.
I have the class EmployeeDto
in the project as well.
package com.azizkale.mockitotutorial.dto;
import java.sql.Date;
public class EmployeeDto {
private Integer id;
private String name;
private String department;
private String gender;
private Date dob;
//constructors, getters, setters
}
DTO (Data Transfer Object):
- DTOs are used to transfer data between different layers of an application, such as between the business logic and the presentation layer, or between different microservices in a distributed system.
- They serve as a container for data that needs to be transferred, encapsulating the fields and providing a way to transport this data without exposing the internal details of the domain objects.
- DTOs play a crucial role in separating concerns by managing the representation of data entities within the domain model and facilitating the transfer of data between different layers of the application.
- The structure of the DTO can be tailored to what is needed in a specific use case, rather than exposing all attributes of the domain object.
- DTOs can help in controlling the information that is exposed outside of the application. They allow for fine-grained control over what data is sent to clients or other services.
And, I have the settings below to be able to use the h2-database.
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
What do mean annotations in the class Employee
?
package com.azizkale.mockitotutorial.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Entity
@Table(name = "db_employee")
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "name")
private String name;
@Column(name = "department")
private String department;
@Column(name = "gender")
private String gender;
@Column(name = "dob")
private Date dob;
}
Entity
: Marks the class as a JPA entity, indicating that instances of this class will be mapped to records in the database.
@Table(name = "db_employee")
: Specifies the name of the database table to which this entity is mapped.
@Id
: Marks the field as the primary key of the entity.
@GeneratedValue(strategy = GenerationType.IDENTITY)
: Configures the way the primary key will be generated. In our case, it indicates that the primary key is an auto-incremented identity column.
@Column(name = "name")
, @Column(name = "department")
, etc.: Specifies the mapping between the entity’s fields and the columns in the corresponding database table.
Lombok Annotations (@AllArgsConstructor
, @NoArgsConstructor
, @Builder
, @Data
): Lombok is a library that helps reduce boilerplate code in Java classes.
@AllArgsConstructor
: Generates a constructor with all fields.@NoArgsConstructor
: Generates a default constructor with no arguments.@Builder
: Implements the builder pattern, providing a convenient way to construct instances of the class.@Data
: Combines various Lombok features, such as@ToString
,@EqualsAndHashCode
, and others. It generates getters, setters,toString
,equals
, andhashCode
methods.- I should add this dependency to use
Lombok
:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
Testing Service Layer
Now, let’s test our methods in the class Service
:
The Function create
In this section, I’ll use Mockito. So, I am adding the dependency Mockito.
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
Now under the path below, I am creating the EmployeeServiceTest
.
src/test/java/com/azizkale/mockitotutorial/service/EmployeeServiceTest:
package com.azizkale.mockitotutorial.service;
//imported packages (you can see these at the project in Github)
@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTest {
// Creating a Mock to mimic EmployeeRepository.
@Mock
private EmployeeRepository employeeRepository;
// An instance of EmployeeServiceImpl, injected with the Mock created with @Mock.
@InjectMocks
private EmployeeServiceImpl employeeService;
// A test method to test the create method of the EmployeeService class.
@Test
public void EmployeeService_CreateEmployee_ReturnsEmployeeDto() {
// Creating an instance of Employee to be used for the test.
Employee employee = new Employee();
// Creating an instance of EmployeeDto to be used for the test.
EmployeeDto employeeDto = EmployeeDto.builder()
.name("Aziz Kale")
.department("IT")
.gender("male")
.dob(new Date())
.build();
// Using BeanUtils.copyProperties to copy properties from EmployeeDto to Employee for the test.
BeanUtils.copyProperties(employeeDto, employee);
// Mocking the employeeRepository.save method to return this instance of Employee whenever any instance of Employee is passed.
when(employeeRepository.save(Mockito.any(Employee.class))).thenReturn(employee);
// Calling the create method of EmployeeService and checking if it returns the expected instance of EmployeeDto.
EmployeeDto savedEmployee = employeeService.create(employeeDto);
// Using AssertJ to check the correctness of the expected values.
Assertions.assertThat(savedEmployee).isNotNull();
}
}
In this test class, a mock is used to mimic the behavior of the EmployeeRepository
. The employeeRepository.save
method is mocked to return a specific instance of Employee
whenever any instance of Employee
is passed. The EmployeeService
's create
method is then called, and AssertJ is used to check whether it returns the expected EmployeeDto
.
Mock
Annotation:
- It is used to create a mock object of a class or interface. In our test class, we don’t want to use the actual implementation of
EmployeeRepository
because we are testing theEmployeeService
class, not the repository. - By annotating a field with
@Mock
, Mockito creates a mock object for that class or interface. In our case,@Mock private EmployeeRepository employeeRepository;
creates a mock of theEmployeeRepository
class.
@InjectMocks
Annotation:
- It is used to automatically inject the mock objects annotated with
@Mock
into the instance of the class that is annotated with@InjectMocks
. - In our test class,
@InjectMocks private EmployeeServiceImpl employeeService;
instructs Mockito to inject the mockEmployeeRepository
into theEmployeeServiceImpl
instance.
The Function findAll
@Test
public void EmployeeService_GetAllEmployee_ReturnsResponseDto(){
// Mocking a Page<Employee> using Mockito
Page<Employee> employees = Mockito.mock(Page.class);
// Configuring the behavior of the employeeRepository mock
// When findAll method of employeeRepository is called with any Pageable, it should return the mocked Page<Employee>.
when(employeeRepository.findAll(Mockito.any(Pageable.class))).thenReturn(employees);
// Calling the findAll method of employeeService with page number 1 and page size 10
EmployeeResponse employeeResponse = employeeService.findAll(1, 10);
// Asserting that the result (saveEmployee) is not null
Assertions.assertThat(employeeResponse).isNotNull();
}
1. Mocking Page<Employee>
: Page<Employee>
is typically used for paginated results. In this test, I created a mock instance of Page
using Mockito. This is a way to simulate the behavior of the Page
class without using a real instance.
2. Mocking Repository Behavior: Using Mockito’s when
method, we configured the behavior of the employeeRepository
mock. I specified that when the findAll
method of employeeRepository
is called with any Pageable
argument, it should return the mocked Page<Employee>
.
3. Calling the Service Method: Invoking the findAll
method of the employeeService
with page number 1 and page size 10. This is the method we are testing.
The Function findById
I want to test this function in two different situations.
1. When an Employee object exists.
@Test
public void EmployeeService_GetEmployeeById_WhenEmployeeExists_ReturnsEmployeeOptional() {
// Arrange
int employeeId = 1;
Employee mockEmployee = Mockito.mock(Employee.class); // create a mock Employee
when(employeeRepository.findById(employeeId)).thenReturn(Optional.of(mockEmployee));
// Act
Optional<Employee> result = employeeService.findById(employeeId);
// Assert
assertTrue(result.isPresent()); // assert that the result is present
assertSame(mockEmployee, result.get()); // assert that the result is the same instance as the mockEmployee
}
2. When an Employee object does not exist.
@Test
public void EmployeeService_GetEmployeeById_WhenEmployeeNotExist_ReturnsOptionalEmpty(){
// Arrange
int employeeId = 2;
when(employeeRepository.findById(employeeId)).thenReturn(Optional.empty());
// Act
Optional<Employee> result = employeeService.findById(employeeId);
// Assert
assertFalse(result.isPresent()); // assert that the result is not present
}
The function update
@Test
public void EmployeeService_UpdateEmployee_ReturnsEmployeeDto(){
int employeeId=1;
Employee employee = Employee.builder()
.id(employeeId).name("Aziz KAle")
.dob(new Date()).gender("Male")
.department("Tesing").build();
EmployeeDto employeeDto = employeeService.mapToDto(employee);
when(employeeRepository.findById(employeeId)).thenReturn(Optional.ofNullable(employee));
when(employeeRepository.save(employee)).thenReturn(employee);
EmployeeDto updatedEmployee = employeeService.update(employeeDto,employeeId);
Assertions.assertThat(updatedEmployee).isNotNull();
}
I checked the basic functionality of the update
method. The test ensures that the method can handle the update operation, and the result is a non-null EmployeeDto
. I used Mocking to isolate the test from the actual database operations and focus on the logic of the service.
The Function delete
@Test
public void EmployeeService_DeleteEmployee_ReturnsVoid() {
int employeeId = 1;
Employee employee = Employee.builder()
.id(employeeId).name("Aziz KAle")
.dob(new Date()).gender("Male")
.department("Tesing").build();
// When findById is invoked with the specified employeeId, it returns an Optional containing the employee.
when(employeeRepository.findById(employeeId)).thenReturn(Optional.ofNullable(employee));
// Configure the delete method to perform no action when called with an Employee object.
doNothing().when(employeeRepository).delete(employee);
// Invoke the delete method of the employeeService with the created employeeId.
employeeService.delete(employeeId);
// Use assertAll to ensure that no exceptions are thrown during the execution of the delete method.
assertAll(() -> employeeService.delete(employeeId));
}
Conclusion
We have tested the service layer of our Java-based RESTful API server. To ensure our tests don’t interfere with actual data, we utilized Mockito, a convenient tool for simulating behaviors. We meticulously named our test functions, not only for testing purposes but also to act as clear documentation for our API. This approach ensures that the API functions as anticipated, and our tests become a valuable guide for anyone using the project.
Thank you for taking the time to read my article.