MockMvc集成测试:模拟Azure存储组件时请求挂起或序列化异常
问题分析与解决方案:文件下载接口MockMvc测试中的InputStreamResource模拟问题
问题现象
- 调用文件下载接口的MockMvc测试时,请求无限挂起;切换到WebTestClient后同样出现超时,问题不局限于测试工具
- 调整配置后测试不再挂起,但抛出
HttpMessageNotWritableException - 已定位问题根源为模拟InputStreamResource的方式不正确
最小复现场景代码
1. 控制器代码
@RestController @RequestMapping("/download") public class DownloadController { @GetMapping public ResponseEntity<InputStreamResource> downloadFile() throws IOException { File file = new File("test.txt"); FileInputStream fis = new FileInputStream(file); InputStreamResource resource = new InputStreamResource(fis); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test.txt\"") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(resource); } }
2. 测试类代码(错误示例)
@SpringBootTest @AutoConfigureMockMvc public class DownloadControllerTest { @Autowired private MockMvc mockMvc; @Test void testDownload() throws Exception { // 错误的InputStreamResource模拟方式 InputStreamResource mockResource = new InputStreamResource(new ByteArrayInputStream("test content".getBytes())); mockMvc.perform(get("/download") .contentType(MediaType.APPLICATION_OCTET_STREAM)) .andExpect(status().isOk()) .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"test.txt\"")) .andExpect(content().bytes("test content".getBytes())); } }
3. POM核心依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
问题根源
- 无限挂起/超时原因:Spring消息转换器处理
InputStreamResource时,会尝试读取流直到结束。如果模拟的流(如ByteArrayInputStream)在测试场景中未被正确消费,或测试工具的流读取逻辑存在阻塞,会导致框架持续等待流数据,最终超时或挂起。 - HttpMessageNotWritableException原因:调整配置后,若模拟的
InputStreamResource关联的流已关闭、不可读,或消息转换器未正确适配InputStreamResource类型,会导致转换器无法写入响应体,抛出该异常。
正确解决方案
方案1:用ByteArrayResource替代模拟InputStreamResource
ByteArrayResource是Spring提供的更适合测试的资源类型,无需处理流生命周期问题:
@Test void testDownload() throws Exception { byte[] content = "test content".getBytes(); mockMvc.perform(get("/download")) .andExpect(status().isOk()) .andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("filename=\"test.txt\""))) .andExpect(content().bytes(content)); }
如果控制器依赖外部文件服务,可通过@MockBean注入模拟服务并返回ByteArrayResource。
方案2:正确模拟InputStreamResource并确保流可被消费
若必须使用InputStreamResource,需保证流可读且未关闭,同时在测试中主动消费响应体:
@Test void testDownloadWithInputStreamResource() throws Exception { mockMvc.perform(get("/download")) .andExpect(status().isOk()) .andDo(result -> { // 手动读取响应体,避免框架阻塞 byte[] responseBytes = result.getResponse().getContentAsByteArray(); assertArrayEquals("test content".getBytes(), responseBytes); }); }
方案3:调整消息转换器配置(复杂场景)
如果项目自定义了消息转换器,需确保ResourceHttpMessageConverter正确配置并支持InputStreamResource:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { ResourceHttpMessageConverter resourceConverter = new ResourceHttpMessageConverter(); resourceConverter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); converters.add(resourceConverter); } }
关键注意事项
- 测试中优先使用
ByteArrayResource或FileSystemResource,避免直接模拟InputStreamResource - 确保测试中的输入流未被提前关闭,且具备可读能力
- 使用MockMvc时,通过
andDo()手动消费响应体可避免流读取阻塞问题
内容的提问来源于stack exchange,提问作者solujic
相关产品推荐
相关产品推荐

