当前位置: 首页 > news >正文

Spring Boot MockMvc实战:高效测试REST API的完整指南

1. 项目概述:为什么我们需要MockMvc?

在开发Spring Boot应用,尤其是RESTful API时,测试环节常常是决定项目质量和开发效率的关键。很多开发者习惯在Postman里点点划划,或者直接启动整个应用,用浏览器或CURL命令来验证接口。这当然能跑通,但问题也很明显:慢、不可靠、难以自动化。想象一下,每次修改一行代码,你都需要花几十秒甚至几分钟重启应用、手动发送请求、再肉眼比对结果,这无疑是对开发热情的极大消耗。更别提当你的服务依赖数据库、外部API或者复杂的业务逻辑时,这种“集成式”测试的脆弱性会指数级上升。

这就是SpringBootTestMockMvc组合拳的价值所在。它们不是为了取代Postman或集成测试,而是为了在单元测试集成测试之间,建立一个高效、轻量、专注的Web层测试屏障。MockMvc允许你在不启动Servlet容器(如Tomcat)的情况下,模拟HTTP请求,并对响应进行断言。这意味着你可以在毫秒级别内完成对一个Controller的完整测试,包括请求路径、参数、头信息、JSON序列化/反序列化、状态码、响应体等所有细节。它测试的是你的代码逻辑,而不是网络环境或服务器状态。

最近的热搜词里频繁出现“自动化测试”、“测试框架”,这正反映了行业从“手工验证”到“质量左移”的转变趋势。对于后端开发者而言,掌握MockMvc是构建可靠CI/CD流水线、践行测试驱动开发(TDD)的基本功。接下来,我将以一个完整的用户管理API为例,带你从零开始,拆解如何用MockMvc构建坚实的测试防线。

2. 环境准备与项目骨架搭建

在开始编写测试之前,我们需要一个清晰的项目结构。这里假设你已经有一个基础的Spring Boot 2.7+(或3.x)项目,并使用了Spring Web和Spring Data JPA(或其他数据层框架)来构建REST API。

2.1 核心依赖引入

首先,确保你的pom.xml(Maven)或build.gradle(Gradle)中包含了必要的测试依赖。对于Spring Boot项目,spring-boot-starter-test是核心,它已经集成了JUnit Jupiter、AssertJ、Hamcrest、Mockito以及我们需要的spring-test模块。

Maven配置示例:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 如果你的Controller返回或接收JSON,确保有Jackson依赖,starter-web通常已包含 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>

关键点解析:

  • scopetest:意味着这些依赖只在运行测试时生效,不会打包进最终的生产环境Jar包,保持部署包的纯净。
  • Spring Boot 3.x注意:从Spring Boot 2.4开始,spring-boot-starter-test默认不再包含junit-vintage-engine(JUnit 4),只包含JUnit 5(JUnit Jupiter)。我们的示例将基于JUnit 5。

2.2 测试代码结构规划

一个清晰的结构有助于维护。我推荐遵循Maven/Gradle的标准目录布局,并在src/test/java下建立与src/main/java平行的包结构。

src ├── main │ ├── java │ │ └── com │ │ └── example │ │ └── demo │ │ ├── DemoApplication.java │ │ ├── controller │ │ │ └── UserController.java │ │ ├── service │ │ │ └── UserService.java │ │ └── repository │ │ └── UserRepository.java │ └── resources │ └── application.properties └── test └── java └── com └── example └── demo └── controller └── UserControllerTest.java // 我们的测试类

这样,UserControllerTest就能方便地访问到被测的UserController

2.3 基础测试类注解理解

在编写第一个测试类之前,必须理解几个核心注解:

  1. @SpringBootTest:这是集成测试的入口注解。它会加载完整的应用程序上下文,模拟启动整个Spring Boot应用。你可以通过webEnvironment属性来控制Web环境。

    • WebEnvironment.MOCK(默认):加载一个Web的ApplicationContext并提供Mock的Servlet环境。这是与MockMvc搭配使用的标准模式,因为它不启动真实服务器。
    • WebEnvironment.RANDOM_PORT:启动一个真实的嵌入式服务器,并监听一个随机端口。这更适合需要测试网络层、过滤器链等完整流程的集成测试。
    • WebEnvironment.NONE:不提供任何Web环境,仅加载普通的ApplicationContext
  2. @AutoConfigureMockMvc:这个注解告诉Spring Boot自动配置一个MockMvc实例,并注入到测试类中。它是连接@SpringBootTestMockMvc的桥梁。

  3. @Test(来自JUnit Jupiter):标记一个方法为测试方法。

有了这些基础,我们就可以开始构建第一个测试了。

3. MockMvc核心API与测试流程拆解

MockMvc的核心思想是“构建请求-执行请求-验证响应”。它的API链式调用非常流畅,是典型的Builder模式。

3.1 初始化MockMvc的三种方式

在测试类中获取MockMvc实例主要有三种方式,各有适用场景:

方式一:自动注入(推荐,最简洁)结合@SpringBootTest@AutoConfigureMockMvc使用。

@SpringBootTest @AutoConfigureMockMvc // 关键注解 class UserControllerTest { @Autowired private MockMvc mockMvc; // 直接注入 // ... 测试方法 }

实操心得:这是最常用、最省心的方式。Spring Boot会自动处理好MockMvc与当前测试应用上下文的绑定。适合绝大多数Controller单元测试场景。

方式二:独立搭建(更轻量,更聚焦)使用MockMvcBuilders.standaloneSetup(...)。这种方式只为指定的一个或多个Controller构建测试环境,不会加载整个应用上下文,速度极快。

@ExtendWith(MockitoExtension.class) // 使用Mockito的扩展 class UserControllerStandaloneTest { private MockMvc mockMvc; @Mock private UserService userService; // 模拟Service层 @InjectMocks private UserController userController; // 将被测Controller注入模拟依赖 @BeforeEach void setUp() { // 只为userController搭建MockMvc环境 mockMvc = MockMvcBuilders.standaloneSetup(userController) .build(); } // ... 测试方法中需要手动设置userService的行为 }

注意事项:这种方式完全隔离了Spring上下文,你需要用@Mock@InjectMocks(来自Mockito)来手动管理Controller的依赖。它适合测试逻辑纯粹、依赖简单的Controller,或者当你只想验证某个特定Controller的映射和序列化逻辑时。

方式三:基于Web应用上下文使用MockMvcBuilders.webAppContextSetup(...)。它需要一个完整的WebApplicationContext

@SpringBootTest class UserControllerWebAppTest { @Autowired private WebApplicationContext webApplicationContext; private MockMvc mockMvc; @BeforeEach void setUp() { mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .build(); } // ... 测试方法 }

这种方式比方式一更显式,但不如方式一简洁。它允许你在构建MockMvc时添加一些全局的配置,比如过滤器。但在大多数情况下,方式一已经足够。

3.2 构建请求:MockMvcRequestBuilders

这是模拟HTTP请求的核心工具类。它提供了静态方法来创建各种类型的请求。

  • GET请求MockMvcRequestBuilders.get("/api/users/{id}", 1L)
  • POST请求MockMvcRequestBuilders.post("/api/users").content(json).contentType(MediaType.APPLICATION_JSON)
  • PUT请求MockMvcRequestBuilders.put("/api/users/{id}", 1L).content(json).contentType(...)
  • DELETE请求MockMvcRequestBuilders.delete("/api/users/{id}", 1L)
  • PATCH、HEAD等:对应的方法。

链式调用配置请求:

mockMvc.perform( MockMvcRequestBuilders .get("/api/users") .param("page", "0") // 添加查询参数 .param("size", "10") .header("Authorization", "Bearer token123") // 添加请求头 .accept(MediaType.APPLICATION_JSON) // 设置Accept头 .characterEncoding("UTF-8") ).andExpect(...);

3.3 验证响应:MockMvcResultMatchers 与 andExpect()

执行请求后,通过andExpect()方法链式地对结果进行断言。MockMvcResultMatchers提供了丰富的匹配器。

  • 状态码status().isOk(),status().isNotFound(),status().isCreated()等。
  • 响应头header().string("Content-Type", MediaType.APPLICATION_JSON_VALUE)
  • 响应体(JSON):这是最复杂的部分,也是测试的重点。
    • jsonPath("$.id").value(1):使用JsonPath表达式提取和断言JSON字段。
    • jsonPath("$.name").value("张三")
    • jsonPath("$.age").value(25)
    • jsonPath("$[*].id").isArray():断言是数组。
    • jsonPath("$", hasSize(2)):断言数组大小(需要导入Hamcrest的hasSize)。
  • 响应体(内容)content().string("success")content().json(expectedJsonString)直接比较JSON字符串。
  • 视图与模型:对于MVC视图,可以用view().name("index")model().attributeExists("user")

3.4 处理响应结果:andReturn()

如果你需要获取响应的详细信息做进一步处理,可以使用andReturn(),它会返回一个MvcResult对象。

MvcResult result = mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andReturn(); String content = result.getResponse().getContentAsString(); int status = result.getResponse().getStatus(); // 可以对content进行更复杂的解析或记录

4. 完整实战:用户管理API测试示例

让我们构建一个完整的UserController及其测试。假设我们有如下简单的REST API:

  • GET /api/users/{id}:根据ID查询用户
  • POST /api/users:创建用户
  • PUT /api/users/{id}:更新用户
  • DELETE /api/users/{id}:删除用户

4.1 实体与Controller代码

为了聚焦测试,我们简化业务逻辑。

User.java (实体)

@Data // Lombok注解,生成getter/setter等 @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String username; private String email; }

UserController.java

@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; // 构造器注入 public UserController(UserService userService) { this.userService = userService; } @GetMapping("/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { return userService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<User> createUser(@Valid @RequestBody User user) { User savedUser = userService.save(user); URI location = ServletUriComponentsBuilder.fromCurrentRequest() .path("/{id}") .buildAndExpand(savedUser.getId()) .toUri(); return ResponseEntity.created(location).body(savedUser); } @PutMapping("/{id}") public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User user) { if (!id.equals(user.getId())) { return ResponseEntity.badRequest().build(); } return ResponseEntity.ok(userService.save(user)); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { userService.deleteById(id); return ResponseEntity.noContent().build(); } }

UserService.java (接口,模拟实现)

public interface UserService { Optional<User> findById(Long id); User save(User user); void deleteById(Long id); }

4.2 测试类完整实现与逐行解析

现在,我们编写对应的UserControllerTest。我们将采用方式一(自动注入),并结合Mockito来模拟UserService,实现真正的单元测试隔离。

// 关键注解:加载测试上下文,并自动配置MockMvc @SpringBootTest @AutoConfigureMockMvc // 使用Mockito的JUnit 5扩展,简化Mock对象管理 @ExtendWith(MockitoExtension.class) class UserControllerTest { @Autowired private MockMvc mockMvc; // 核心测试工具 @MockBean // Spring特有的注解,用于在应用上下文中Mock一个Bean private UserService userService; private User testUser; private String testUserJson; // 在每个测试方法执行前运行,用于准备测试数据 @BeforeEach void setUp() throws JsonProcessingException { testUser = new User(1L, "testUser", "test@example.com"); // 使用Jackson的ObjectMapper将对象转为JSON字符串,用于POST/PUT请求体 ObjectMapper objectMapper = new ObjectMapper(); testUserJson = objectMapper.writeValueAsString(testUser); } // 测试场景1:成功获取用户 @Test void getUserById_ShouldReturnUser_WhenUserExists() throws Exception { // 1. 定义Mock行为:当userService.findById(1L)被调用时,返回包含testUser的Optional given(userService.findById(1L)).willReturn(Optional.of(testUser)); // 2. 执行GET请求并断言 mockMvc.perform(MockMvcRequestBuilders.get("/api/users/{id}", 1L) .accept(MediaType.APPLICATION_JSON)) // 声明客户端接受JSON .andExpect(MockMvcResultMatchers.status().isOk()) // 断言HTTP 200 .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1L)) // 使用JsonPath断言响应体JSON .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("testUser")) .andExpect(MockMvcResultMatchers.jsonPath("$.email").value("test@example.com")) .andDo(MockMvcResultHandlers.print()); // 可选:打印详细的请求和响应信息,调试时非常有用 } // 测试场景2:获取不存在的用户 @Test void getUserById_ShouldReturn404_WhenUserNotExists() throws Exception { // 定义Mock行为:返回空的Optional given(userService.findById(999L)).willReturn(Optional.empty()); mockMvc.perform(MockMvcRequestBuilders.get("/api/users/{id}", 999L)) .andExpect(MockMvcResultMatchers.status().isNotFound()); // 断言HTTP 404 } // 测试场景3:成功创建用户 @Test void createUser_ShouldReturn201AndUser() throws Exception { // 定义Mock行为:当save被调用时,返回带ID的testUser given(userService.save(any(User.class))).willReturn(testUser); mockMvc.perform(MockMvcRequestBuilders.post("/api/users") .contentType(MediaType.APPLICATION_JSON) // 必须设置Content-Type .content(testUserJson) // 请求体 .accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isCreated()) // 断言HTTP 201 Created .andExpect(MockMvcResultMatchers.header().exists("Location")) // 断言响应头包含Location .andExpect(MockMvcResultMatchers.jsonPath("$.id").exists()) // 断言响应体有id字段 .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("testUser")); } // 测试场景4:创建用户 - 验证请求体校验失败(@Valid) @Test void createUser_ShouldReturn400_WhenInputInvalid() throws Exception { // 构造一个无效的用户对象(例如,username为空) User invalidUser = new User(null, "", "invalid-email"); String invalidJson = new ObjectMapper().writeValueAsString(invalidUser); // 注意:这里不需要定义userService.save的行为,因为请求在进入Controller方法前就会因校验失败而返回400。 mockMvc.perform(MockMvcRequestBuilders.post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(invalidJson)) .andExpect(MockMvcResultMatchers.status().isBadRequest()); // 断言HTTP 400 } // 测试场景5:成功更新用户 @Test void updateUser_ShouldReturnUpdatedUser() throws Exception { User updatedInfo = new User(1L, "updatedUser", "updated@example.com"); String updatedJson = new ObjectMapper().writeValueAsString(updatedInfo); given(userService.save(any(User.class))).willReturn(updatedInfo); mockMvc.perform(MockMvcRequestBuilders.put("/api/users/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) .content(updatedJson)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("updatedUser")); } // 测试场景6:更新用户 - ID不匹配 @Test void updateUser_ShouldReturn400_WhenIdMismatch() throws Exception { // 请求路径ID是1,但请求体中的用户ID是2 User userWithWrongId = new User(2L, "test", "test@test.com"); String wrongIdJson = new ObjectMapper().writeValueAsString(userWithWrongId); mockMvc.perform(MockMvcRequestBuilders.put("/api/users/{id}", 1L) .contentType(MediaType.APPLICATION_JSON) .content(wrongIdJson)) .andExpect(MockMvcResultMatchers.status().isBadRequest()); // 验证userService.save没有被调用(可选,更严格的测试) then(userService).shouldHaveNoInteractions(); } // 测试场景7:成功删除用户 @Test void deleteUser_ShouldReturn204() throws Exception { // 对于void方法,使用doNothing来定义Mock行为 willDoNothing().given(userService).deleteById(1L); mockMvc.perform(MockMvcRequestBuilders.delete("/api/users/{id}", 1L)) .andExpect(MockMvcResultMatchers.status().isNoContent()); // 断言HTTP 204 No Content // 验证方法确实被调用了一次 then(userService).should(times(1)).deleteById(1L); } }

4.3 代码深度解析与技巧

  1. @MockBeanvs@Mock:这是Spring Boot测试中的一个关键点。@MockBean(来自spring-boot-test)会将一个Mock对象注册到Spring的应用上下文中,替换掉原有的同名Bean。这确保了在测试UserController时,它注入的是我们模拟的userService。而普通的@Mock(来自Mockito)只是创建一个Mock对象,不会自动注入到Spring上下文中,通常与@InjectMocks在独立测试中使用。

  2. JSON序列化与ObjectMapper:在测试中,我们经常需要将Java对象转换为JSON字符串作为请求体。直接拼接字符串容易出错且难以维护。使用Spring Boot自动配置的ObjectMapper(它遵循了你的应用配置,如日期格式)是最佳实践。可以通过@Autowired注入,也可以在@BeforeEach中手动创建。示例中采用了手动创建,确保测试的独立性。

  3. given().willReturn()willDoNothing().given():这是Mockito的BDD风格API(BDDMockito类),比传统的when().thenReturn()读起来更自然。given设定前提(模拟行为),willReturn指定返回值。对于无返回值的方法,使用willDoNothing()

  4. any(User.class):这是一个参数匹配器。当你不关心调用方法时传入的具体参数值,只关心方法是否被调用以及返回什么时,可以使用它。但要注意,如果方法逻辑依赖于参数的特定属性,你可能需要更精确的匹配,如eq(testUser)或使用ArgumentMatchers.argThat()进行自定义匹配。

  5. andDo(MockMvcResultHandlers.print()):这是一个极其有用的调试工具。当测试失败时,它会将完整的请求和响应信息(包括头信息、体内容)打印到控制台。我强烈建议在编写和调试测试时加上它,一旦测试稳定,可以考虑移除以避免日志噪音。

  6. 验证Mock交互:使用then().should()来验证模拟对象的方法是否被按预期调用。这对于测试删除、更新等具有“副作用”的操作非常有用,确保业务逻辑确实触发了对Service层的调用。

5. 高级技巧与常见问题排查

掌握了基础测试后,我们来看看一些进阶场景和那些容易踩的坑。

5.1 测试异常处理与全局控制器建议

Controller中通常会使用@ExceptionHandler@ControllerAdvice来处理异常。MockMvc也能很好地测试这些异常处理逻辑。

假设我们有一个全局异常处理器:

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) { ErrorResponse error = new ErrorResponse("NOT_FOUND", ex.getMessage()); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); } }

在测试中,你只需要让userService.findById()抛出对应的异常,然后断言返回的HTTP状态码和错误体即可。

@Test void getUserById_ShouldReturnCustomError_WhenExceptionThrown() throws Exception { given(userService.findById(1L)).willThrow(new ResourceNotFoundException("User not found")); mockMvc.perform(get("/api/users/1")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.code").value("NOT_FOUND")) .andExpect(jsonPath("$.message").value("User not found")); }

5.2 测试文件上传与下载

文件上传是另一个常见场景。MockMvc提供了MockMultipartFile来模拟文件。

测试文件上传:

@Test void uploadFile_ShouldSuccess() throws Exception { MockMultipartFile file = new MockMultipartFile( "file", // 参数名,需与@RequestParam名称一致 "test.txt", MediaType.TEXT_PLAIN_VALUE, "Hello, World!".getBytes() ); mockMvc.perform(multipart("/api/upload").file(file)) .andExpect(status().isOk()); }

测试文件下载:你需要验证响应头(如Content-Disposition)和响应体内容。

@Test void downloadFile_ShouldReturnFile() throws Exception { given(fileService.loadFileAsResource("somefile.txt")).willReturn(someResource); MvcResult result = mockMvc.perform(get("/api/download/somefile.txt")) .andExpect(status().isOk()) .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("attachment"))) .andExpect(content().contentType(MediaType.APPLICATION_OCTET_STREAM)) .andReturn(); // 可以进一步断言响应体字节内容 byte[] content = result.getResponse().getContentAsByteArray(); assertThat(content).isNotEmpty(); }

5.3 测试安全端点(如JWT认证)

如果API受Spring Security保护,测试会稍微复杂。你需要模拟一个已认证的用户。

方法一:使用@WithMockUser等注解(推荐)Spring Security Test提供了便捷的注解。

@Test @WithMockUser(username = "admin", roles = {"USER", "ADMIN"}) // 模拟一个具有角色的用户 void getSecuredResource_ShouldReturnOk_WhenAuthenticated() throws Exception { mockMvc.perform(get("/api/secured")) .andExpect(status().isOk()); } @Test @WithAnonymousUser // 模拟匿名用户 void getSecuredResource_ShouldReturnUnauthorized_WhenAnonymous() throws Exception { mockMvc.perform(get("/api/secured")) .andExpect(status().isUnauthorized()); // 或 isForbidden() }

方法二:手动设置SecurityContext对于更复杂的场景,可以在测试方法内手动设置。

@Test void testWithManualSecurity() throws Exception { UserDetails user = User.withUsername("user").password("pass").roles("USER").build(); SecurityContext context = SecurityContextHolder.createEmptyContext(); context.setAuthentication(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities())); SecurityContextHolder.setContext(context); // ... 执行测试 mockMvc.perform(get("/api/secured")); SecurityContextHolder.clearContext(); // 清理,避免影响其他测试 }

5.4 常见问题与排查技巧实录

问题1:测试报错java.lang.IllegalArgumentException: Not a managed type

  • 症状:启动测试上下文时失败,提示某个实体类不是被管理的类型。
  • 原因@SpringBootTest会扫描整个应用上下文。如果你的测试类所在的包路径比@SpringBootApplication主类所在的包更“上层”或更“偏”,可能导致组件扫描不到某些配置(如JPA实体)。
  • 解决
    1. 最佳实践:将测试类放在与主应用类相同的包或其子包下。Spring Boot默认会从主类所在包开始扫描。
    2. 使用@SpringBootTest(classes = YourApplication.class)显式指定主配置类。
    3. 使用@DataJpaTest等切片测试注解替代@SpringBootTest,如果你只想测试Repository层。

问题2:@MockBean导致其他集成测试变慢或上下文重复加载

  • 症状:测试套件运行缓慢,每个测试类似乎都重新加载了Spring上下文。
  • 原因:Spring Test默认会缓存测试上下文。但如果两个测试类的上下文配置不同(例如,使用了不同的@MockBean组合),Spring就无法重用缓存,必须重新加载。
  • 解决
    1. 重构测试:尽量将需要相同Mock Bean配置的测试放在同一个测试类中。
    2. 使用@TestConfiguration:将一组固定的Mock Bean定义在一个内部的@TestConfiguration静态类中,然后在多个测试类中导入它,有助于上下文缓存。
    3. 接受现实:对于真正的单元测试,考虑使用MockMvcBuilders.standaloneSetup(...),它完全避免了Spring上下文的加载,速度最快。

问题3:JSON比较失败,因为字段顺序或格式不一致

  • 症状:使用content().json()比较JSON字符串时失败,尽管逻辑上内容相同。
  • 原因:JSON对象的键值对顺序在标准中是无序的,但字符串比较是严格的。
  • 解决
    1. 优先使用jsonPath():它只关心你指定的路径是否存在且值匹配,不关心整体JSON字符串和字段顺序。
    2. 如果必须比较完整JSON,可以使用content().json(expectedJson, strictMode),并将strictMode设为false(宽松模式),它会忽略扩展字段和数组顺序。但需谨慎使用。
    3. 使用第三方库如JSONAssert,在测试中引入依赖并进行断言:JSONAssert.assertEquals(expectedJson, actualJson, false);

问题4:测试多线程或异步控制器(如返回DeferredResult,Callable

  • 症状:测试直接返回,没有等到异步结果就断言,导致断言失败。
  • 原因MockMvc默认是同步处理的。对于异步端点,需要特殊处理。
  • 解决:使用MockMvcasyncDispatch
@Test void testAsyncEndpoint() throws Exception { MvcResult mvcResult = mockMvc.perform(get("/api/async")) .andExpect(request().asyncStarted()) // 先断言异步已开始 .andReturn(); // 等待异步处理完成,并获取最终结果 mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(status().isOk()) .andExpect(content().string("async result")); }

问题5:如何测试Controller中的@RequestParam,@PathVariable,@RequestHeader

  • 解决:这些在构建请求时直接设置即可,前面示例已有体现。
    • @RequestParam: 使用.param("key", "value")
    • @PathVariable: 在URL路径中直接体现,如get("/api/users/{id}", 1L)
    • @RequestHeader: 使用.header("Header-Name", "value")
    • @CookieValue: 使用.cookie(new Cookie("name", "value"))
    • @RequestBody: 使用.content(jsonString).contentType(MediaType.APPLICATION_JSON)

掌握这些技巧和排查方法,你就能应对日常开发中99%的Controller测试场景。记住,好的测试应该是快速、独立、可重复、自验证的。MockMvc正是帮助我们实现这一目标的利器。花时间写好测试,虽然在初期会感觉慢了,但它带来的代码质量信心和长期维护效率的提升,绝对是值得的。

http://www.cnnetsun.cn/news/3092258.html

相关文章:

  • 用心理学原理强化AI工程纪律:权威、承诺与社会认同的实战框架
  • Mythos门控发布:大模型推理深度与责任治理的双重跃迁
  • Anthropic Mythos:可信推理链与门控式能力发布解析
  • Claude推理中间层‘蒸发’:模型内核如何替代Router Layer
  • AI系统五大核心组件:告别大模型幻觉的工程化方案
  • LLM Agent生产就绪:确定性输出与可观测性工程实践
  • URL参数优化实战:从性能瓶颈到体验提升的完整策略
  • ChatGPT核心技术解析与工程实践指南
  • Claude Mythos门控机制解析:如何工程化驾驭大模型推理能力
  • Mythos推理模组:大模型可验证推理能力的门控式演进
  • Golang配置文件加密实战:从AES-256到KMS集成
  • Anthropic Zero-Layer:大模型应用中‘意图对齐层’的消失与工程范式重构
  • OpenSSL实战:RSA密钥对生成与公钥提取全流程详解
  • AI指令设计五步法:从提问到指挥的工程化实践
  • GPT-5.5 Pro:面向真实工作的AI执行者,不是聊天框而是工位同事
  • 安全日志审计Web页面高效使用指南:从登录到实战分析
  • Deepseek v3:10倍降本的前沿大模型架构解析
  • RAG论文深度解析:知识密集型任务的范式迁移与工程落地
  • 渗透测试学习路径全解析:从零基础到实战精通的完整指南
  • Mythos门控式发布:长上下文推理的可控能力释放机制
  • 基于TPAFE0808和TM4C129的多通道信号采集系统设计
  • AI模型服务安全部署:从0.0.0.0监听地址到纵深防御实战指南
  • 基于Si4731与PIC18LF4553的可编程收音机系统设计
  • 苹果GenAI三层架构:3B端侧模型、私有云大模型与Siri集成实战
  • GPT-4实为8专家协同系统:揭秘MoE架构与动态路由机制
  • Audacity:从音频新手到专业编辑的完整成长指南
  • MagiskHide Props Config终极指南:10分钟掌握设备指纹伪装技巧
  • 嘎嘎降AI双引擎技术解密:为什么它能把论文AI率稳定压到5%以下(9大平台验证)
  • 使用xUnit为WingetUI插件构建自动化测试框架:从单元测试到CI/CD集成
  • Claude底层架构解析:长上下文稳定性与宪法式对齐设计