skip to content

How do you test a file-upload (multipart) endpoint with MockMvc?

level: middleimportance: should knowfreq 45%

answer

  1. multipart("/upload"), not post(...)
  2. MockMultipartFile(name, filename, contentType, bytes)
  3. name MUST match @RequestParam/@RequestPart
  4. multipart defaults to POST — override for PUT
  5. container size limits not enforced in MockMvc

basics

~10 s

Use MockMvcRequestBuilders.multipart("/upload") and attach a MockMultipartFile with .file(...). The MockMultipartFile's first constructor arg (the name) must match the controller's @RequestParam / @RequestPart name.

solid answer

~30 s

For multipart endpoints, build the request with MockMvcRequestBuilders.multipart("/upload") (a MockMultipartHttpServletRequestBuilder) instead of post(). Attach uploaded parts with .file(MockMultipartFile) — the MockMultipartFile takes (parameterName, originalFilename, contentType, bytes), and the parameterName must exactly match the controller's @RequestParam("file") MultipartFile or @RequestPart name, or Spring can't bind it. Non-file form fields go through .param(...) or additional .file(...) for JSON parts. You can chain multiple .file(...) calls for multiple uploads. Then assert as usual with andExpect(status().isOk()). Note multipart(...) defaults to POST; use .with(request -> { request.setMethod("PUT"); return request; }) for PUT uploads. This still runs entirely in-memory — no real HTTP.

code

java · 15 lines
java
@Test
void uploadsCsv() throws Exception {
    MockMultipartFile file = new MockMultipartFile(
            "file", "data.csv", "text/csv",
            "id,name\n1,Ada".getBytes(StandardCharsets.UTF_8));

    mockMvc.perform(multipart("/uploads")
                .file(file)
                .param("owner", "ada"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.rows").value(1));
}
// controller: @PostMapping("/uploads")
//   String upload(@RequestParam("file") MultipartFile file,
//                 @RequestParam("owner") String owner)

go deeper

for a junior

Can attach a MockMultipartFile with multipart(...).file(...).

for a middle

Matches part names correctly and mixes file + form params / JSON parts.

for a senior

Knows what multipart parsing MockMvc does and does not exercise vs a real container.

for a principal

Decides where upload-limit/streaming concerns get real-container coverage vs MockMvc slices.

**Multipart / file-upload endpoints** in Spring MVC bind uploaded files to `MultipartFile` parameters, typically `@RequestParam("file") MultipartFile file` or `@RequestPart("file") MultipartFile file`, and often mix in regular form fields. **Building the request — `MockMvcRequestBuilders.multipart(uri)`** returns a `MockMultipartHttpServletRequestBuilder`. Key points: - It sets the method to **POST** and content type to `multipart/form-data` automatically. - `.file(MockMultipartFile)` attaches an uploaded file part. - `.file(name, byte[])` is a shorthand overload. - `.param(name, value)` adds ordinary form fields alongside files. - You can call `.file(...)` multiple times for multiple uploads (e.g. `@RequestParam("files") List<MultipartFile>`). **`MockMultipartFile`** is the mock upload. Its main constructor is `MockMultipartFile(String name, String originalFilename, String contentType, byte[] content)`. The **`name`** (first arg) is the *form field / part name* and **must match** the controller's `@RequestParam`/`@RequestPart` name — mismatch means the argument isn't bound (you get null or a missing-parameter 400, depending on `required`). `originalFilename` is what `MultipartFile.getOriginalFilename()` returns; `contentType` is the part's MIME type. ```java MockMultipartFile file = new MockMultipartFile( "file", // must match @RequestParam("file") "report.csv", // original filename "text/csv", // content type "a,b,c\n1,2,3".getBytes()); // bytes mockMvc.perform(multipart("/upload").file(file).param("owner", "ada")) .andExpect(status().isOk()); ``` **JSON + file combined:** a common pattern is `@RequestPart("meta") MetaDto meta` (JSON) plus `@RequestPart("file") MultipartFile file`. Attach the JSON part as another `MockMultipartFile` with content type `application/json`: ```java MockMultipartFile meta = new MockMultipartFile( "meta", "", "application/json", "{\"title\":\"Q1\"}".getBytes()); mockMvc.perform(multipart("/upload").file(file).file(meta))... ``` **Changing the HTTP method:** `multipart(...)` defaults to POST. For a PUT/PATCH upload, override the method: ```java mockMvc.perform(multipart("/docs/1") .file(file) .with(req -> { req.setMethod("PUT"); return req; })) ``` (the `with(RequestPostProcessor)` hook mutates the underlying `MockHttpServletRequest`). **Gotchas:** - **Name mismatch is the #1 failure** — the `MockMultipartFile` name must equal the controller's part name. - The `StandardServletMultipartResolver` / multipart parsing that a real container does is *not* fully exercised — MockMvc injects the parts directly. So you're testing your controller and binding, not container-level multipart limits (`max-file-size`, `max-request-size` from `spring.servlet.multipart.*` aren't enforced here). - `@WebMvcTest` auto-configures multipart support; in a bare `standaloneSetup` you may need nothing extra because MockMvc builds the multipart request itself. - CSRF: with Spring Security active, a POST needs `.with(csrf())` or it'll be rejected.

  • Why might the uploaded file bind as null even though the test attaches one?
    The MockMultipartFile's name (first constructor argument) doesn't match the controller's @RequestParam/@RequestPart part name. Spring binds by part name, so a mismatch leaves the parameter unbound.
  • Does MockMvc enforce spring.servlet.multipart.max-file-size?
    No. Those limits are enforced by the container's multipart resolver during real parsing. MockMvc injects parts directly, so you'd test size limits via an integration test with a real server.

saying these in an interview costs you the question

  • Using post(...).content(bytes) for a multipart endpoint instead of multipart(...).file(...)
  • Assuming MockMvc enforces container max-file-size limits
  • Not realizing multipart() defaults to POST

context