Spring Boot 提供了不同的测试方法,例如单元测试、集成测试和端到端测试。本文我们将学习如何使用 Junit 5 和 Mockito 为 Spring Boot 应用程序编写单元测试。
什么是单元测试
单元测试是一种软件测试技术,其中对软件应用程序的各个单元或组件进行单独测试,以验证它们是否按预期运行。
Spring Boot Starter 测试模块
Spring Boot Starter Test 依赖项提供了用于测试 Spring Boot 应用程序的基本库和实用程序,包括 JUnit、Hamcrest、Mockito 和 AssertJ。它简化了 Spring Boot 应用程序的编写和执行测试的过程。
什么是 JUnit 和 AssertJ
JUnit 是一个流行的 Java 开源测试框架。它提供注释来定义测试方法、测试类和测试套件,以及用于验证预期结果的断言。 JUnit使得编写和执行单元测试变得容易,保证了Java代码的正确性。
AssertJ 是一个流行的 Java 库,它提供流畅、富有表现力的断言,用于编写更清晰、更具可读性的单元测试。它还提供了有用的错误消息。
动手试试
使用 spring 初始化工具创建一个项目:
项目结构:
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/unit_test_db?useSSL=false
spring.datasource.username=root
spring.datasource.password=Online12@
spring.jpa.hibernate.ddl-auto = update
Employee(员工)实体:
import jakarta.persistence.*;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "employees")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false)
private String firstName;
@Column(nullable = false)
private String lastName;
@Column(nullable = false)
private String email;
}
Employee Repository:
import org.springframework.data.jpa.repository.JpaRepository;
import test.example.springboot.test.demo.Model.Employee;
import java.util.Optional;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByEmail(String email);
}
Employee Controller
@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<Optional<Employee>> getEmployeeById(@PathVariable("id") long id){
return new ResponseEntity<Optional<Employee>>(employeeService.getEmployeeById(id),HttpStatus.OK);
}
@PutMapping("{id}")
public ResponseEntity<Employee> updateEmployee(@PathVariable("id") long id,
@RequestBody Employee employee){
return new ResponseEntity<Employee>(employeeService.updateEmployee(employee,id),HttpStatus.OK);
}
@DeleteMapping("{id}")
public ResponseEntity<String> deleteEmployee(@PathVariable("id") long id){
employeeService.deleteEmployee(id);
return new ResponseEntity<String>("Employee deleted successfully!.", HttpStatus.OK);
}
}
Employee Service 接口和实现
public interface EmployeeService {
Employee saveEmployee(Employee employee);
List<Employee> getAllEmployees();
Optional<Employee> getEmployeeById(long id);
Employee updateEmployee(Employee employee,long id);
void deleteEmployee(long id);
}
@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 RuntimeException("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 employee,long id) {
Employee existingEmployee = employeeRepository.findById(id)
.orElseThrow(()->new RuntimeException());
existingEmployee.setFirstName(employee.getFirstName());
existingEmployee.setLastName(employee.getLastName());
existingEmployee.setEmail(employee.getEmail());
employeeRepository.save(existingEmployee);
return existingEmployee;
}
@Override
public void deleteEmployee(long id) {
employeeRepository.deleteById(id);
}
}
用 JUnit 编写存储库层的单元测试
@DataJpaTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EmployeeRepositoryUnitTests {
@Autowired
private EmployeeRepository employeeRepository;
@Test
@DisplayName("Test 1:Save Employee Test")
@Order(1)
@Rollback(value = false)
public void saveEmployeeTest(){
//Action
Employee employee = Employee.builder()
.firstName("Sam")
.lastName("Curran")
.email("sam@gmail.com")
.build();
employeeRepository.save(employee);
//Verify
System.out.println(employee);
Assertions.assertThat(employee.getId()).isGreaterThan(0);
}
@Test
@Order(2)
public void getEmployeeTest(){
//Action
Employee employee = employeeRepository.findById(1L).get();
//Verify
System.out.println(employee);
Assertions.assertThat(employee.getId()).isEqualTo(1L);
}
@Test
@Order(3)
public void getListOfEmployeesTest(){
//Action
List<Employee> employees = employeeRepository.findAll();
//Verify
System.out.println(employees);
Assertions.assertThat(employees.size()).isGreaterThan(0);
}
@Test
@Order(4)
@Rollback(value = false)
public void updateEmployeeTest(){
//Action
Employee employee = employeeRepository.findById(1L).get();
employee.setEmail("samcurran@gmail.com");
Employee employeeUpdated = employeeRepository.save(employee);
//Verify
System.out.println(employeeUpdated);
Assertions.assertThat(employeeUpdated.getEmail()).isEqualTo("samcurran@gmail.com");
}
@Test
@Order(5)
@Rollback(value = false)
public void deleteEmployeeTest(){
//Action
employeeRepository.deleteById(1L);
Optional<Employee> employeeOptional = employeeRepository.findById(1L);
//Verify
Assertions.assertThat(employeeOptional).isEmpty();
}
}
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) 根据@Order 注解指定的顺序指定测试执行的顺序。
您可以为您的项目使用 MySQL 或任何其他数据库来保存实际数据。如果你使用同一个数据库进行测试,将会影响你的实际数据。因此,您可以使用内存H2数据库进行测试。这是常见的方式。
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
更改 application.properties 文件:
#MysQL Database Configuration for Project
spring.datasource.url=jdbc:mysql://localhost:3306/unit_test_db?useSSL=false
spring.datasource.username=root
spring.datasource.password=Online12@
spring.jpa.hibernate.ddl-auto = update
#H2 Database Configuration for testing
spring.datasource.test.url=jdbc:h2:mem/unit_test_db
spring.datasource.test.diver-class-name=org.h2.Driver
spring.datasource.test.username=sa
spring.datasource.test.password=password
spring.jpa.test.hibernate.ddl-auto = create-drop
运行EmployeeRepositoryUnitTest.java类:
使用Mockito
Mockito 是一个流行的 Java 框架,用于在单元测试中模拟对象。 Mockito 可以在单元测试中使用来模拟依赖关系并隔离正在测试的代码。Controller通常依赖Service层组件来执行业务逻辑或与应用程序的数据层(例如存储库)交互。使用 Mockito 模拟服务层组件,将正在测试的控制器与实际服务层实现隔离。
MockMvc 不是 Mockito 的一部分。 MockMvc是Spring Test框架提供的专门用于测试Spring MVC控制器的类。
EmployeeControllerUnitTests类:
@WebMvcTest(EmployeeController.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EmployeeControllerUnitTests {
@Autowired
private MockMvc mockMvc;
@MockBean
private EmployeeService employeeService;
@Autowired
private ObjectMapper objectMapper;
Employee employee;
@BeforeEach
public void setup(){
employee = Employee.builder()
.id(1L)
.firstName("John")
.lastName("Cena")
.email("john@gmail.com")
.build();
}
//Post Controller
@Test
@Order(1)
public void saveEmployeeTest() throws Exception{
// precondition
given(employeeService.saveEmployee(any(Employee.class))).willReturn(employee);
// action
ResultActions response = mockMvc.perform(post("/api/employees")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(employee)));
// verify
response.andDo(print()).
andExpect(status().isCreated())
.andExpect(jsonPath("$.firstName",
is(employee.getFirstName())))
.andExpect(jsonPath("$.lastName",
is(employee.getLastName())))
.andExpect(jsonPath("$.email",
is(employee.getEmail())));
}
//Get Controller
@Test
@Order(2)
public void getEmployeeTest() throws Exception{
// precondition
List<Employee> employeesList = new ArrayList<>();
employeesList.add(employee);
employeesList.add(Employee.builder().id(2L).firstName("Sam").lastName("Curran").email("sam@gmail.com").build());
given(employeeService.getAllEmployees()).willReturn(employeesList);
// action
ResultActions response = mockMvc.perform(get("/api/employees"));
// verify the output
response.andExpect(status().isOk())
.andDo(print())
.andExpect(jsonPath("$.size()",
is(employeesList.size())));
}
//get by Id controller
@Test
@Order(3)
public void getByIdEmployeeTest() throws Exception{
// precondition
given(employeeService.getEmployeeById(employee.getId())).willReturn(Optional.of(employee));
// action
ResultActions response = mockMvc.perform(get("/api/employees/{id}", employee.getId()));
// verify
response.andExpect(status().isOk())
.andDo(print())
.andExpect(jsonPath("$.firstName", is(employee.getFirstName())))
.andExpect(jsonPath("$.lastName", is(employee.getLastName())))
.andExpect(jsonPath("$.email", is(employee.getEmail())));
}
//Update employee
@Test
@Order(4)
public void updateEmployeeTest() throws Exception{
// precondition
given(employeeService.getEmployeeById(employee.getId())).willReturn(Optional.of(employee));
employee.setFirstName("Max");
employee.setEmail("max@gmail.com");
given(employeeService.updateEmployee(employee,employee.getId())).willReturn(employee);
// action
ResultActions response = mockMvc.perform(put("/api/employees/{id}", employee.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(employee)));
// verify
response.andExpect(status().isOk())
.andDo(print())
.andExpect(jsonPath("$.firstName", is(employee.getFirstName())))
.andExpect(jsonPath("$.email", is(employee.getEmail())));
}
// delete employee
@Test
public void deleteEmployeeTest() throws Exception{
// precondition
willDoNothing().given(employeeService).deleteEmployee(employee.getId());
// action
ResultActions response = mockMvc.perform(delete("/api/employees/{id}", employee.getId()));
// then - verify the output
response.andExpect(status().isOk())
.andDo(print());
}
}
运行测试类:
使用 JUnit + Mockito 编写服务层单元测试
EmployeeServiceUnitTests类:
@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EmployeeServiceUnitTests {
@Mock
private EmployeeRepository employeeRepository;
@InjectMocks
private EmployeeServiceImpl employeeService;
private Employee employee;
@BeforeEach
public void setup(){
employee = Employee.builder()
.id(1L)
.firstName("John")
.lastName("Cena")
.email("john@gmail.com")
.build();
}
@Test
@Order(1)
public void saveEmployeeTest(){
// precondition
given(employeeRepository.save(employee)).willReturn(employee);
//action
Employee savedEmployee = employeeService.saveEmployee(employee);
// verify the output
System.out.println(savedEmployee);
assertThat(savedEmployee).isNotNull();
}
@Test
@Order(2)
public void getEmployeeById(){
// precondition
given(employeeRepository.findById(1L)).willReturn(Optional.of(employee));
// action
Employee existingEmployee = employeeService.getEmployeeById(employee.getId()).get();
// verify
System.out.println(existingEmployee);
assertThat(existingEmployee).isNotNull();
}
@Test
@Order(3)
public void getAllEmployee(){
Employee employee1 = Employee.builder()
.id(2L)
.firstName("Sam")
.lastName("Curran")
.email("sam@gmail.com")
.build();
// precondition
given(employeeRepository.findAll()).willReturn(List.of(employee,employee1));
// action
List<Employee> employeeList = employeeService.getAllEmployees();
// verify
System.out.println(employeeList);
assertThat(employeeList).isNotNull();
assertThat(employeeList.size()).isGreaterThan(1);
}
@Test
@Order(4)
public void updateEmployee(){
// precondition
given(employeeRepository.findById(employee.getId())).willReturn(Optional.of(employee));
employee.setEmail("max@gmail.com");
employee.setFirstName("Max");
given(employeeRepository.save(employee)).willReturn(employee);
// action
Employee updatedEmployee = employeeService.updateEmployee(employee,employee.getId());
// verify
System.out.println(updatedEmployee);
assertThat(updatedEmployee.getEmail()).isEqualTo("max@gmail.com");
assertThat(updatedEmployee.getFirstName()).isEqualTo("Max");
}
@Test
@Order(5)
public void deleteEmployee(){
// precondition
willDoNothing().given(employeeRepository).deleteById(employee.getId());
// action
employeeService.deleteEmployee(employee.getId());
// verify
verify(employeeRepository, times(1)).deleteById(employee.getId());
}
}
运行测试类:
小结
在本教程中,我们使用 JUnit 5 和 Mockito 为存储库、控制器和服务编写了 Spring Boot 单元测试。许多经验丰富的程序员和团队都会优先考虑单元测试和集成测试,以确保应用程序健壮、可靠和可维护。
没有回复内容