In this tutorial, we will learn how to perform unit testing Spring boot CRUD RESTful web services using JUnit 5 and Mockito framework.
Check out my Spring boot testing Udemy course: Testing Spring Boot Application with JUnit and Mockito (Includes Testcontainers)
Spring boot provides spring-boot-starter-test dependency for unit testing and integration testing of Spring boot application:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
The Spring Boot Starter Test dependency is a primary dependency for testing the Spring Boot Applications. It holds all the necessary elements required for the testing.
For the Unit testing controller layer REST APIs, we gonna use the following testing libraries:
- JUnit 5 Framework
- Mockito 4 (Latest)
- Hamcrest framework
- AssertJ Library
- JsonPath Library
JUnit 5 Framework
Mockito 4 (Latest)
Hamcrest framework
AssertJ Library
JsonPath Library
@WebMvcTest Annotation
SpringBoot provides @WebMvcTest annotation to test Spring MVC Controllers. Also, @WebMvcTest based test run faster because it will load only the specified controller and its dependencies only without loading the entire application.
Spring Boot instantiates only the web layer rather than the whole application context. In an application with multiple controllers, you can even ask for only one to be instantiated by using, for example, @WebMvcTest(HomeController.class).
Tools and technologies used
- Java 11+
- Spring Boot
- Lombok
- JUnit 5 Framework
- Hamcrest
- AssertJ
- JsonPath
- Mockito
- IntelliJ IDEA
- Docker
- Maven
1. Create Spring Boot Application
- Spring Web
- Spring Data JPA
- Lombok
2. Maven Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
3. Create JPA Entity
import lombok.*;
import javax.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;
}
4. Create Repository Layer
import net.javaguides.springboot.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
5. Create Service Layer
EmployeeService
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
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 EmployeeRepository employeeRepository;
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 exist 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);
}
}
6. Controller Layer - CRUD REST APIs
import net.javaguides.springboot.model.Employee;
import net.javaguides.springboot.service.EmployeeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/employees")
public class EmployeeController {
private EmployeeService employeeService;
public EmployeeController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Employee createEmployee(@RequestBody Employee employee){
return employeeService.saveEmployee(employee);
}
@GetMapping
public List<Employee> getAllEmployees(){
return employeeService.getAllEmployees();
}
@GetMapping("{id}")
public ResponseEntity<Employee> getEmployeeById(@PathVariable("id") long employeeId){
return employeeService.getEmployeeById(employeeId)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PutMapping("{id}")
public ResponseEntity<Employee> updateEmployee(@PathVariable("id") long employeeId,
@RequestBody Employee employee){
return employeeService.getEmployeeById(employeeId)
.map(savedEmployee -> {
savedEmployee.setFirstName(employee.getFirstName());
savedEmployee.setLastName(employee.getLastName());
savedEmployee.setEmail(employee.getEmail());
Employee updatedEmployee = employeeService.updateEmployee(savedEmployee);
return new ResponseEntity<>(updatedEmployee, HttpStatus.OK);
})
.orElseGet(() -> ResponseEntity.notFound().build());
}
@DeleteMapping("{id}")
public ResponseEntity<String> deleteEmployee(@PathVariable("id") long employeeId){
employeeService.deleteEmployee(employeeId);
return new ResponseEntity<String>("Employee deleted successfully!.", HttpStatus.OK);
}
}
7. Writing Unit Tests for CRUD REST API's
import com.fasterxml.jackson.databind.ObjectMapper; import net.javaguides.springboot.model.Employee; import net.javaguides.springboot.service.EmployeeService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import java.util.ArrayList; import java.util.List; import java.util.Optional; import static org.hamcrest.CoreMatchers.is; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @WebMvcTest public class EmployeeControllerTests { @Autowired private MockMvc mockMvc; @MockBean private EmployeeService employeeService; @Autowired private ObjectMapper objectMapper; @Test public void givenEmployeeObject_whenCreateEmployee_thenReturnSavedEmployee() throws Exception{ // given - precondition or setup Employee employee = Employee.builder() .firstName("Ramesh") .lastName("Fadatare") .email("ramesh@gmail.com") .build(); given(employeeService.saveEmployee(any(Employee.class))) .willAnswer((invocation)-> invocation.getArgument(0)); // when - action or behaviour that we are going test ResultActions response = mockMvc.perform(post("/api/employees") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(employee))); // then - verify the result or output using assert statements response.andDo(print()). andExpect(status().isCreated()) .andExpect(jsonPath("$.firstName", is(employee.getFirstName()))) .andExpect(jsonPath("$.lastName", is(employee.getLastName()))) .andExpect(jsonPath("$.email", is(employee.getEmail()))); } // JUnit test for Get All employees REST API @Test public void givenListOfEmployees_whenGetAllEmployees_thenReturnEmployeesList() throws Exception{ // given - precondition or setup List<Employee> listOfEmployees = new ArrayList<>(); listOfEmployees.add(Employee.builder().firstName("Ramesh").lastName("Fadatare").email("ramesh@gmail.com").build()); listOfEmployees.add(Employee.builder().firstName("Tony").lastName("Stark").email("tony@gmail.com").build()); given(employeeService.getAllEmployees()).willReturn(listOfEmployees); // when - action or the behaviour that we are going test ResultActions response = mockMvc.perform(get("/api/employees")); // then - verify the output response.andExpect(status().isOk()) .andDo(print()) .andExpect(jsonPath("$.size()", is(listOfEmployees.size()))); } // positive scenario - valid employee id // JUnit test for GET employee by id REST API @Test public void givenEmployeeId_whenGetEmployeeById_thenReturnEmployeeObject() throws Exception{ // given - precondition or setup long employeeId = 1L; Employee employee = Employee.builder() .firstName("Ramesh") .lastName("Fadatare") .email("ramesh@gmail.com") .build(); given(employeeService.getEmployeeById(employeeId)).willReturn(Optional.of(employee)); // when - action or the behaviour that we are going test ResultActions response = mockMvc.perform(get("/api/employees/{id}", employeeId)); // then - verify the output response.andExpect(status().isOk()) .andDo(print()) .andExpect(jsonPath("$.firstName", is(employee.getFirstName()))) .andExpect(jsonPath("$.lastName", is(employee.getLastName()))) .andExpect(jsonPath("$.email", is(employee.getEmail()))); } // negative scenario - valid employee id // JUnit test for GET employee by id REST API @Test public void givenInvalidEmployeeId_whenGetEmployeeById_thenReturnEmpty() throws Exception{ // given - precondition or setup long employeeId = 1L; Employee employee = Employee.builder() .firstName("Ramesh") .lastName("Fadatare") .email("ramesh@gmail.com") .build(); given(employeeService.getEmployeeById(employeeId)).willReturn(Optional.empty()); // when - action or the behaviour that we are going test ResultActions response = mockMvc.perform(get("/api/employees/{id}", employeeId)); // then - verify the output response.andExpect(status().isNotFound()) .andDo(print()); } // JUnit test for update employee REST API - positive scenario @Test public void givenUpdatedEmployee_whenUpdateEmployee_thenReturnUpdateEmployeeObject() throws Exception{ // given - precondition or setup long employeeId = 1L; Employee savedEmployee = Employee.builder() .firstName("Ramesh") .lastName("Fadatare") .email("ramesh@gmail.com") .build(); Employee updatedEmployee = Employee.builder() .firstName("Ram") .lastName("Jadhav") .email("ram@gmail.com") .build(); given(employeeService.getEmployeeById(employeeId)).willReturn(Optional.of(savedEmployee)); given(employeeService.updateEmployee(any(Employee.class))) .willAnswer((invocation)-> invocation.getArgument(0)); // when - action or the behaviour that we are going test ResultActions response = mockMvc.perform(put("/api/employees/{id}", employeeId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(updatedEmployee))); // then - verify the output response.andExpect(status().isOk()) .andDo(print()) .andExpect(jsonPath("$.firstName", is(updatedEmployee.getFirstName()))) .andExpect(jsonPath("$.lastName", is(updatedEmployee.getLastName()))) .andExpect(jsonPath("$.email", is(updatedEmployee.getEmail()))); } // JUnit test for update employee REST API - negative scenario @Test public void givenUpdatedEmployee_whenUpdateEmployee_thenReturn404() throws Exception{ // given - precondition or setup long employeeId = 1L; Employee savedEmployee = Employee.builder() .firstName("Ramesh") .lastName("Fadatare") .email("ramesh@gmail.com") .build(); Employee updatedEmployee = Employee.builder() .firstName("Ram") .lastName("Jadhav") .email("ram@gmail.com") .build(); given(employeeService.getEmployeeById(employeeId)).willReturn(Optional.empty()); given(employeeService.updateEmployee(any(Employee.class))) .willAnswer((invocation)-> invocation.getArgument(0)); // when - action or the behaviour that we are going test ResultActions response = mockMvc.perform(put("/api/employees/{id}", employeeId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(updatedEmployee))); // then - verify the output response.andExpect(status().isNotFound()) .andDo(print()); } // JUnit test for delete employee REST API @Test public void givenEmployeeId_whenDeleteEmployee_thenReturn200() throws Exception{ // given - precondition or setup long employeeId = 1L; willDoNothing().given(employeeService).deleteEmployee(employeeId); // when - action or the behaviour that we are going test ResultActions response = mockMvc.perform(delete("/api/employees/{id}", employeeId)); // then - verify the output response.andExpect(status().isOk()) .andDo(print()); } }
If you want to understand in detail then I highly suggest my Udemy course: Testing Spring Boot Application with JUnit and Mockito (Includes Testcontainers)
8. Demo
9. Conclusion
In this tutorial, we have discussed how to perform Spring Boot unit testing CRUD REST APIs using JUnit and Mockito frameworks.
If you want to learn more about Spring boot testing then highly suggest my Udemy course: Testing Spring Boot Application with JUnit and Mockito (Includes Testcontainers)
10. Spring Boot Testing Tutorials and Guides
- Spring Boot Testing - Data Access Layer Integration Testing using Testcontainers
- Spring Boot Testing - REST API Integration Testing using Testcontainers
- Spring Data JPA Repository Testing using Spring Boot @DataJpaTest
- CRUD JUnit Tests for Spring Data JPA - Testing Repository Layer
- Spring Boot Integration Testing MySQL CRUD REST API Tutorial
Comments
Post a Comment
Leave Comment