You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 23:45:14