Spring Reactive集成测试:非阻塞向WebTestClient添加FilePart的方法
集成测试中WebTestClient非阻塞上传FilePart的正确方式
问题场景
控制器代码
@RestController @RequestMapping("/customers/profile") class CustomerProfileVerificationController(private val CustomerProfileVerificationService: ProfileVerificationService) { @PostMapping("/verify", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) fun verifyCustomerProfile( @RequestPart incomingImage: FilePart, ): Mono<CustomerProfileVerificationResult> = CustomerProfileVerificationService.executeCustomerProfileVerification(incomingImage) }
原测试代码及错误
原测试代码尝试构建Multipart请求体,但运行时触发了Reactor BlockHound的阻塞调用错误:
@Test fun `given a valid request then return the verification result`() { val partName = "incomingImage" val builder = MultipartBodyBuilder() builder.part(partName, ClassPathResource("/test/resources/source-1"), MediaType.MULTIPART_FORM_DATA) val parts = builder.build().toSingleValueMap() // When calling the endpoint webTestClient .post() .uri("/customers/profile/verify") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .accept(MediaType.APPLICATION_JSON) .body(Mono.just(parts), FilePart::class.java) .exchange() .expectStatus() .isOk }
核心错误栈信息:
org.springframework.web.reactive.function.client.WebClientRequestException: Blocking call! java.io.FileInputStream#readBytes ... Caused by: reactor.blockhound.BlockingOperationError: Blocking call! java.io.FileInputStream#readBytes at java.base/java.io.FileInputStream.readBytes(FileInputStream.java) ... at org.springframework.util.MimeTypeUtils.generateMultipartBoundary(MimeTypeUtils.java:390)
错误原因
- 原代码将
MultipartBodyBuilder构建的MultiValueMap转成单值Map后,强行指定FilePart::class.java作为body类型,违反了Reactive非阻塞的处理逻辑,触发阻塞式文件读取。 - 资源路径写法错误:
ClassPathResource的路径无需额外添加/test/resources前缀,该目录本身就是类路径的一部分。 - 手动设置
Content-Type头,导致框架触发阻塞的Multipart边界生成逻辑。
正确的非阻塞写法
方式一:使用MultipartBodyBuilder+BodyInserters
@Test fun `given a valid request then return the verification result`() { val partName = "incomingImage" val builder = MultipartBodyBuilder() // 资源路径直接写文件名,文件需放在src/test/resources下 builder.part(partName, ClassPathResource("source-1"), MediaType.IMAGE_JPEG) val multipartData = builder.build() webTestClient .post() .uri("/customers/profile/verify") .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromMultipartData(multipartData)) .exchange() .expectStatus() .isOk }
方式二:简化版直接构建请求体
跳过MultipartBodyBuilder,直接通过BodyInserters添加文件part:
@Test fun `given a valid request then return the verification result`() { webTestClient .post() .uri("/customers/profile/verify") .accept(MediaType.APPLICATION_JSON) .body(BodyInserters.fromMultipartData( "incomingImage", ClassPathResource("source-1"), MediaType.IMAGE_JPEG )) .exchange() .expectStatus() .isOk }
关键注意事项
- 不要手动设置
Content-Type头:BodyInserters.fromMultipartData会自动处理multipart/form-data类型及非阻塞的边界生成。 - 确保资源路径正确:
ClassPathResource("source-1")对应src/test/resources/source-1文件。 - 指定正确的单个文件媒体类型:比如图片用
MediaType.IMAGE_JPEG,而非整个请求的MediaType.MULTIPART_FORM_DATA。
内容的提问来源于stack exchange,提问作者Rafael Reyes
相关产品推荐
相关产品推荐

