在今天的学习中,我们将深入研究JUnit和Mockito,这是Java开发中最强大的单元测试工具之一。通过学习如何编写清晰、高效的单元测试,我们将揭开单元测试的神秘面纱,助力你在项目中写出更健壮的代码。
提示: 今天的代码是在第九天代码的基础上进行开发,我们将为UserController中添加更多的单元测试方法,以展示JUnit和Mockito的强大功能。
核心知识介绍:
Unit 5 的主要特性和注解:@Test:标记方法作为测试方法。@BeforeEach / @AfterEach:分别表示在每个测试方法前后运行的方法。@BeforeAll / @AfterAll:分别表示在所有测试开始之前和所有测试结束之后只运行一次的方法。@DisplayName:为测试类或测试方法定义一个自定义的显示名称。@Nested:表示内部类,其成员方法可以作为嵌套的测试类进行分组。@Tag:为测试方法添加标签,可以用来过滤测试执行。@ExtendWith:用来注册自定义扩展,例如可以用来集成 Spring TestContext Framework。@Disabled:禁用某个测试方法或类。
JUnit 5 断言和假设:Assertions 类提供了一系列的静态方法来声明断言,如 assertEquals, assertTrue, assertAll 等。Assumptions 类提供了静态方法来声明测试的前提条件,如 assumeTrue。Mockito 的主要特性和注解:@Mock:创建一个模拟对象。@InjectMocks:自动注入模拟对象到被测试的类中。@Spy:创建一个真实对象的包装,可以模拟某些方法的行为。@Captor:创建一个参数捕获器,用于捕获方法调用的参数。
@TestMethodOrder 是一个类型级别的注解,用于指定测试类中测试方法的执行顺序。它需要与 MethodOrderer 接口的实现类一起使用,JUnit 提供了几种不同的方法排序器,如按名称、注解、随机等。
@Order 是一个方法级别的注解,用于指定测试方法的执行顺序。当测试类上使用了 @TestMethodOrder(OrderAnnotation.class) 注解时,你可以在每个测试方法上使用 @Order 来定义它们的执行顺序。
以下是一些常用的 MethodOrderer 实现:
OrderAnnotation:根据测试方法上的 @Order 注解来指定执行顺序。测试方法通过 @Order 注解的值(一个整数)来定义它们的执行顺序。Alphanumeric:按照测试方法名称的字母数字顺序执行。这个顺序首先考虑数字,然后是字母。MethodName:按照方法名称的字典顺序(即字符串顺序)执行。Random:每次执行时都按照随机顺序执行测试方法。这有助于发现由于测试方法间的依赖关系而产生的潜在问题。DisplayName:按照测试方法的显示名称(@DisplayName 注解指定的值)的字典顺序执行。
代码示例:
在今天的代码示例中,我们将在昨天的基础上进一步完善UserController的单元测试,使用JUnit和Mockito来验证控制器层的方法是否按照预期执行。
在 pom.xml 文件增加增加测试依赖
org.springframework.boot
spring-boot-starter-test
3.1.6
org.junit.vintage
junit-vintage-engine
org.hamcrest
hamcrest
2.2
UserControllerTest.java
package com.icoderoad.springboot60days.day9.controller;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.icoderoad.springboot60days.day9.entity.User;
import com.icoderoad.springboot60days.day9.service.UserService;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.Arrays;
import java.util.List;
@ExtendWith(MockitoExtension.class)
@WebMvcTest(UserController.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
private User user;
@BeforeEach
void setUp() {
user = new User();
user.setId(1L);
user.setUsername("Test User");
user.setEmail("test@example.com");
}
/**
* 验证UserController的getAllUsers方法正常获取所有用户信息。
*/
@Test
@Order(4)
public void getAllUsersTest() throws Exception {
List users = Arrays.asList(user);
when(userService.list()).thenReturn(users);
mockMvc.perform(get("/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].username", is(user.getUsername())));
}
/**
* 验证UserController的createUser方法正常创建用户。
*/
@Test
@Order(1)
public void createUserTest() throws Exception {
when(userService.saveOrUpdate(any(User.class))).thenReturn(true);;
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk());
verify(userService, times(1)).save(any(User.class));
}
/**
* 验证UserController的getUserById方法正常获取指定ID的用户信息。
*/
@Test
@Order(2)
public void getUserByIdTest() throws Exception {
when(userService.getById(user.getId())).thenReturn(user);
mockMvc.perform(get("/users/{id}", user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.username", is(user.getUsername())));
}
/**
* 验证UserController的updateUserById方法正常更新指定ID的用户信息。
*/
@Test
@Order(3)
public void updateUserByIdTest() throws Exception {
when(userService.saveOrUpdate(any(User.class))).thenReturn(true);;
mockMvc.perform(put("/users/{id}", user.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk());
verify(userService, times(1)).updateById(any(User.class));
}
/**
* 验证UserController的deleteUserById方法正常删除指定ID的用户。
*/
@Test
@Order(5)
public void deleteUserByIdTest() throws Exception {
when(userService.removeById(user.getId())).thenReturn(true);;
mockMvc.perform(delete("/users/{id}", user.getId()))
.andExpect(status().isOk());
verify(userService, times(1)).removeById(user.getId());
}
}
当天学习知识总结:
在今天的学习中,我们深入研究了单元测试,并利用 Mockito 框架加强了测试的功能。通过学习如何编写JUnit5测试以及使用Mockito模拟依赖,我们揭开了单元测试的神秘面纱,为更健壮的代码打下了坚实的基础。
在代码示例中,我们创建了一个 UserControllerTest 类,使用了 Mockito 注解和特性。主要注解包括 @Mock 用于创建模拟对象,@InjectMocks 用于创建被测试类的实例并自动注入模拟对象,@Spy 用于创建 Spy 对象,@Captor 用于捕获方法参数,以及 @RunWith(MockitoJUnitRunner.class) 用于在 JUnit 测试中运行 Mockito 测试。
通过这些注解和特性,我们能够编写清晰、高效的单元测试,验证控制器层的各个方法的行为是否符合预期。其中,我们测试了获取所有用户、创建用户、获取指定ID用户、更新用户、删除用户等方法,以确保它们在不同情况下能够正确执行。
总体而言,通过今天的学习,我们不仅深入了解了单元测试的基本原理,还学会了如何在Spring Boot项目中使用JUnit5和Mockito框架进行测试,为后续更复杂的业务逻辑和代码改动提供了可靠的测试基础。在接下来的学习中,我们将继续