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

MockRestServiceServer测试Multipart-formdata文件上传的问题排查

解决Multipart Form Data请求的单元测试问题

针对你遇到的两个核心问题,我来一步步帮你解决:

问题1:处理动态变化的Content-Type(含boundary和charset)

当发送multipart请求时,Spring的RestTemplate会自动生成唯一的boundary并添加到Content-Type头中,所以测试时没法用固定字符串做精确断言。我们可以用Hamcrest匹配器来检查Content-Type是否包含multipart/form-data,不用纠结动态生成的boundary部分。

修改测试类中的createServiceStub方法,把固定的Content-Type断言替换成匹配器:

import org.hamcrest.Matchers;
import org.springframework.http.HttpHeaders;

private ResponseActions createServiceStub(HttpMethod method, String url) { // 移除固定contenType参数
    String encodedCredentials = Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
    ResponseActions stub = server.expect(ExpectedCount.once(), requestTo(url));
    stub = stub.andExpect(method(method));
    // 断言Content-Type包含multipart/form-data,可选验证charset
    stub = stub.andExpect(header(HttpHeaders.CONTENT_TYPE, 
        Matchers.allOf(
            Matchers.containsString(MediaType.MULTIPART_FORM_DATA_VALUE),
            Matchers.containsString("charset=UTF-8") // 根据你的实际需求选择是否保留
        )));
    stub = stub.andExpect(header("Authorization", "Basic " + encodedCredentials));
    return stub;
}

另外建议调整Service类的createRequestHeaders方法:发送multipart请求时,不需要手动设置Content-Type,RestTemplate会自动处理(包括生成合法的boundary)。你可以改成:

// 修改后的createRequestHeaders
private HttpHeaders createRequestHeaders(JiraClient jiraClient) { // 移除contentType参数
    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    headers.setBasicAuth(jiraClient.getUsername(), jiraClient.getPassword());
    return headers;
}

// 在addAttachment中调用
HttpHeaders headers = createRequestHeaders(jiraClient);
headers.set("X-Atlassian-Token", "no-check");
// 不再手动设置ContentType,交给RestTemplate自动处理

这样RestTemplate会自动生成正确的Content-Type头,测试时用上面的匹配器断言即可。

问题2:断言请求的Payload内容一致

要验证multipart请求中的文件内容和预期一致,我们需要解析请求的multipart内容,逐个检查每个part的文件名、内容等信息。可以通过自定义RequestMatcher实现:

在你的测试方法中添加payload断言逻辑:

import org.springframework.mock.web.MockMultipartHttpServletRequest;
import org.springframework.mock.web.MockMultipartFile;
import java.nio.file.Files;
import java.nio.file.Paths;

@Test
public void addAttachment_WithValidData_ShouldAddAttachmentToJiraIssue() throws Exception {
    String url = host + "/rest/api/2/issue/" + issueKey + "/attachments";
    ResponseActions stub = createServiceStub(HttpMethod.POST, url);
    stub = stub.andExpect(header("X-Atlassian-Token", "no-check"));
    
    // 添加Payload断言:验证文件part的内容和预期一致
    stub = stub.andExpect(request -> {
        // 将请求转换为MockMultipartHttpServletRequest以便解析
        MockMultipartHttpServletRequest multipartRequest = new MockMultipartHttpServletRequest();
        multipartRequest.setContent(request.getBody());
        multipartRequest.setContentType(request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
        
        // 获取名为"file"的part
        MockMultipartFile filePart = (MockMultipartFile) multipartRequest.getFile("file");
        assertThat(filePart).isNotNull("请求中必须包含file部分");
        
        // 验证文件名
        assertThat(filePart.getOriginalFilename())
            .isEqualTo(attachmentFileSystemResource.getFilename());
        
        // 验证文件内容和预期的resource一致
        byte[] expectedContent = Files.readAllBytes(Paths.get(validJiraAttachmentAddition.getURI()));
        assertThat(filePart.getBytes()).isEqualTo(expectedContent);
    });
    
    stub.andRespond(withSuccess());
    
    when(mockJiraAttachment.downloadFileFromWeb()).thenReturn(attachmentFileSystemResource);
    jiraService.addAttachment(mockJiraClient, mockJiraIssue, mockJiraAttachment);
}

补充说明:

  • 确保attachmentFileSystemResource指向你的测试文件,validJiraAttachmentAddition是预期的文件内容资源。
  • 如果需要验证多个part,在request lambda中继续获取其他part并添加断言即可。

完整修改后的测试类关键片段

@ActiveProfiles("test")
@RestClientTest(JiraService.class)
public class JiraServiceTest {
    @Value("classpath:jira/add_attachment/validJiraAttachmentAddition.json")
    private Resource validJiraAttachmentAddition;
    
    @Autowired
    private MockRestServiceServer server;
    
    @Autowired
    private JiraService jiraService;
    
    @Mock
    private JiraAttachment mockJiraAttachment;
    
    private FileSystemResource attachmentFileSystemResource;
    private String host = "https://your-jira-test-instance.com";
    private String issueKey = "TEST-123";
    private String username = "test-user";
    private String password = "test-pass";
    
    @BeforeEach
    public void setupTests() throws IOException {
        // 初始化测试文件资源和mock
        attachmentFileSystemResource = new FileSystemResource("src/test/resources/test-attachment.txt");
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    public void addAttachment_WithValidData_ShouldAddAttachmentToJiraIssue() throws Exception {
        String url = host + "/rest/api/2/issue/" + issueKey + "/attachments";
        ResponseActions stub = createServiceStub(HttpMethod.POST, url);
        stub = stub.andExpect(header("X-Atlassian-Token", "no-check"));
        
        // 断言请求文件内容
        stub = stub.andExpect(request -> {
            MockMultipartHttpServletRequest multipartRequest = new MockMultipartHttpServletRequest();
            multipartRequest.setContent(request.getBody());
            multipartRequest.setContentType(request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE));
            
            MockMultipartFile filePart = (MockMultipartFile) multipartRequest.getFile("file");
            assertThat(filePart).isNotNull();
            assertThat(filePart.getOriginalFilename()).isEqualTo(attachmentFileSystemResource.getFilename());
            
            byte[] expectedContent = Files.readAllBytes(Paths.get(validJiraAttachmentAddition.getURI()));
            assertThat(filePart.getBytes()).isEqualTo(expectedContent);
        });
        
        stub.andRespond(withSuccess());
        
        when(mockJiraAttachment.downloadFileFromWeb()).thenReturn(attachmentFileSystemResource);
        jiraService.addAttachment(mockJiraClient, mockJiraIssue, mockJiraAttachment);
    }
    
    private ResponseActions createServiceStub(HttpMethod method, String url) {
        String encodedCredentials = Base64.getEncoder().encodeToString((username + ":" + password).getBytes());
        ResponseActions stub = server.expect(ExpectedCount.once(), requestTo(url));
        stub = stub.andExpect(method(method));
        stub = stub.andExpect(header(HttpHeaders.CONTENT_TYPE, 
            Matchers.allOf(
                Matchers.containsString(MediaType.MULTIPART_FORM_DATA_VALUE),
                Matchers.containsString("charset=UTF-8")
            )));
        stub = stub.andExpect(header("Authorization", "Basic " + encodedCredentials));
        return stub;
    }
}

这样就能解决你遇到的两个问题:既可以正确断言动态的Content-Type头,又能验证请求中的文件内容是否符合预期。

内容的提问来源于stack exchange,提问作者Pedro Pereira

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 19:42:44