RestTestClient 发送 multipart 数据时为何提示缺少 value?
在基于 MockMvc 的测试模式下,RestTestClient 对 multipart 的支持取决于 Spring Framework 版本。对于 Spring Framework 7.0.x,示例中的现象符合已知限制:客户端可以生成 multipart 请求体,但 MockMvc 无法像 Servlet 容器那样解析这些字节。可以使用 MockMvc 的 multipart 请求构造器,也可以启动真实服务器后通过 RestTestClient 测试。官方问题 #35569
为什么有请求体,仍然提示缺少 value?
示例中的客户端通过以下方式创建:
RestTestClient.bindToApplicationContext(context).build();
这种绑定方式通过 MockMvc 使用模拟请求和响应,不会启动真实 HTTP 服务器。RestTestClient 官方说明
multipart 请求涉及两个环节:客户端将数据序列化成 multipart 字节,服务端再解析这些字节,生成供 @RequestPart 读取的 part。日志中的 Content-Length: [199] 只能说明存在请求体,不能证明解析已经完成。
在上述支持受限的版本中,MockMvc 缺少 Servlet 容器负责的 multipart 解析过程。因此,即使请求体存在,@RequestPart("value") 仍可能找不到对应的 part。这也能解释为什么启动服务器后,使用 Postman 可以正常调用。官方问题中的原因说明
先确认版本标注
需要确认材料中的 Spring v4.0.4 指的是 Spring Boot 版本还是 Spring Framework 版本。RestTestClient 从 Spring Framework 7.0 才开始提供,因此不能将这段代码直接视为 Spring Framework 4.0.4 的示例。RestTestClient API
应检查项目实际解析到的 spring-test、spring-web 和 spring-webmvc 版本。Spring Framework 7.1 的发布说明明确列出了 RestTestClient 与 MockMvc 的 multipart 请求支持改进。如果项目已经使用包含该改进的版本,还需要检查请求内容及自定义配置,不能仅凭同一个异常认定原因相同。Spring Framework 7.1 发布说明
方案一:使用 MockMvc 显式构造 part
如果目的是验证 Controller 能否接收 value 并返回结果,可以保留 MOCK 环境,使用 multipart() 和 MockPart:
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockPart;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class TestMultipartWithMockMvc {
private MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext context) {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@Test
void sendMultiPart() throws Exception {
MockPart valuePart = new MockPart(
"value", "test".getBytes(StandardCharsets.UTF_8));
valuePart.getHeaders().setContentType(MediaType.TEXT_PLAIN);
mockMvc.perform(multipart("/api/multipart").part(valuePart))
.andExpect(status().isOk())
.andExpect(jsonPath("$.value").value("test"));
}
}
这个测试直接提供构造好的 part。MockMvc 的 multipart 测试使用 MockMultipartHttpServletRequest,不会实际解析网络传输中的 multipart 请求体,因此适合验证参数绑定和 Controller 行为,不能替代对真实容器解析过程的验证。MockMvc 请求构造说明
方案二:启动真实服务器,继续使用 RestTestClient
如果需要覆盖 multipart 编码、HTTP 传输和 Servlet 容器解析,应将测试环境改成随机端口,并把客户端绑定到服务器。
将测试类上的注解改为:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
在测试类中注入端口,并替换客户端初始化方法:
@LocalServerPort
private int port;
@BeforeEach
void setupApi() {
client = RestTestClient.bindToServer()
.baseUrl("http://localhost:" + port)
.build();
}
其中 LocalServerPort 的完整类名是 org.springframework.boot.test.web.server.LocalServerPort。bindToServer() 会连接实际运行的服务器;如果只修改 webEnvironment,仍使用 bindToApplicationContext(context),客户端还是会走 MockMvc。RestTestClient 绑定方式说明
请求代码可以继续使用 MultiValueMap,同时修正响应断言:
@Test
void sendMultiPart() {
String value = "test";
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("value", value);
client.post()
.uri("/api/multipart")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(body)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.value").isEqualTo(value);
}
示例中还需要修正什么?
- 日志记录的是
/api/parts,Controller 和测试代码使用的是/api/multipart。应统一路径,再用同一次请求的日志进行排查。 - 原来的 JSON 断言字符串缺少结尾的
}。这不会导致缺少 part,但在请求成功后会影响断言。上面的示例改用 JSONPath,避免手动拼接 JSON。 - 不要通过
@RequestPart(required = false)隐藏异常。接口需要value时,应修复测试请求或解析过程。 - 如果真实服务器测试也失败,再检查实际请求中是否存在名为
value的 part,以及应用的 multipart 配置。仅凭Content-Type和请求体长度无法判断 part 内容是否正确。
备注:内容仅供参考。
