How to test file upload and download in Spring MVC with MockMvc
MockMvc gives Spring MVC tests a fast, deterministic way to exercise controllers without starting a live servlet container. It still runs the dispatcher, argument resolvers, validation, status handling, and response rendering. That makes it suitable for endpoints that accept multipart uploads, return binary content, or both, while keeping each test focused on the web layer.
File transfers fail at boundaries that ordinary JSON tests rarely touch: multipart boundaries, filenames, content types, empty bodies, large streams, and response headers. MockMvc lets you construct those conditions precisely, inspect both the request seen by a controller and the response returned to a client, and reproduce them in a local build or CI pipeline.
Australian teams often work across Sydney, Melbourne, Brisbane, and regional offices, so upload and download timing can change with latency and daylight-saving shifts. The local context also appears in fixtures: an Australian GST invoice may use AUD and a date such as 08/03/2026, while a customer document may contain an organisation’s name. Those details are useful for testing encoding and metadata without exposing real customer data.
A reliable suite separates the HTTP contract from storage and external services. It checks what the controller accepts, what it hands to the application service, and what a client can retrieve later. The practical split is:
| Area | Upload case | Download case |
|---|---|---|
| Request construction | multipart(...) and MockMultipartFile |
get(...) or the endpoint’s HTTP method |
| Main assertion | Binding, validation, and service hand-off | Status, headers, and returned bytes |
| Typical isolation | Mock the storage or processing service | Mock the source of the downloadable content |
| Limitation | Does not prove browser or network behaviour | Does not prove infrastructure throughput |
How MockMvc represents multipart uploads
MockMvc models an upload with MockMultipartFile, a multipart request builder, and the same resolvers used by a running Spring application. The file part carries a field name, original filename, declared media type, and content. A controller can receive it through @RequestParam or @RequestPart, while additional fields travel as ordinary request parameters. This lets a test mirror the precise form payload sent by a browser or API client.
A small upload test can look like this:
byte[] pdf = "report bytes".getBytes(StandardCharsets.UTF_8);
MockMultipartFile report = new MockMultipartFile(
"file",
"quarterly-report.pdf",
MediaType.APPLICATION_PDF_VALUE,
pdf
);
mockMvc.perform(multipart("/api/reports")
.file(report)
.param("category", "finance")
.param("amount", "AUD 1250.00"))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/reports/42"));
After the request, verify that the service received the expected filename, category, and byte count; don’t infer those facts solely from a 201 Created response. Missing parts, unsupported media types, and filenames containing spaces or non-ASCII characters deserve separate cases. Multipart requests are easy to assemble incorrectly, so assert the field name against the controller contract rather than copying it from habit.
Testing upload validation and storage boundaries
Validation tests should cover the failures a controller is responsible for rejecting. Depending on the application contract, an empty file, a file above the configured limit, a missing required part, or an unexpected media type may produce 400 Bad Request, 413 Payload Too Large, or 415 Unsupported Media Type. Keep those outcomes in the controller test, and move content scanning or persistence details behind mocked service boundaries.
When a partner performs malware scanning or document verification, architecture choices such as supplier co-location can affect latency, retries, and timeout budgets. MockMvc should still verify that the upload reaches the collaboration point, that a rejected document returns the agreed status, and that the original file is not processed twice.
Use synthetic Australian business data where possible, such as invoice-2026-03-08.pdf, an amount of AUD 1,250.00, and a date entered as 08/03/2026. Assert how the controller stores or forwards those values rather than imposing a locale it did not promise. For persistence checks, replace a mocked repository with a temporary directory or container-backed store in a separate integration test; a controller slice alone cannot prove that bytes were written correctly.
Verifying download headers and exact file content
A download test should establish more than a successful status. Check the content type, Content-Disposition, optional Content-Length, cache controls, and any authorisation-sensitive filename. The filename header matters because browsers use it when saving a file, and it can reveal encoding errors even when the payload itself is correct.
MvcResult result = mockMvc.perform(get("/api/reports/42"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_PDF))
.andExpect(header().string(
HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"quarterly-report.pdf\""))
.andReturn();
assertThat(result.getResponse().getContentAsByteArray())
.isEqualTo(expectedPdfBytes);
Compare binary responses byte for byte; converting them to a string can corrupt content or conceal differences. For large fixtures, a checksum can make the assertion more practical, while a small known file still supports exact equality. If the endpoint uses StreamingResponseBody, ResponseEntity<Resource>, or asynchronous processing, follow the required async dispatch and assert the final committed response rather than an intermediate state.
Mocking external boundaries without hiding defects
MockMvc is strongest as a web-layer test. Mock the service that writes to object storage, a database, or the filesystem, then verify the interaction that the controller promises. If legacy code reads configuration through a static helper, this explanation of mocking static methods explains when Mockito’s supported mechanisms fit and when refactoring is safer. Avoid using deep mocks to conceal an unclear ownership boundary.
Keep a narrow integration layer beneath the slice test: one test can use a temporary directory to prove upload and retrieval, while another can exercise a real database or storage adapter. Many Australian enterprise teams coordinate delivery with Atlassian tools such as Bitbucket Pipelines or Bamboo, so fast MockMvc checks can run before slower integration stages without changing the deployment workflow.
For documents containing personal information, synthetic fixtures should remain the default. Australian teams handling personal data also need to consider guidance from the OAIC and the Australian Cyber Security Centre, including safe transfer practices and incident preparation. These controls do not belong in a mocked unit test, but they shape which files are allowed into test environments and how failures are reported.
A dependable file transfer testing strategy
A compact checklist keeps file-flow tests useful as the application changes. The goal is not a large collection of near-identical happy paths; it is a set of distinct contracts covering valid input, rejected input, and byte-preserving retrieval.
- Use deterministic fixtures: Generate or store known bytes, then calculate the expected length or digest once.
- Match the multipart contract: Keep field names, filename, media type, and extra parameters aligned with the endpoint.
- Test meaningful rejections: Cover empty, oversized, missing, malformed, and unsupported files where relevant.
- Verify the service hand-off: Confirm metadata and content reach the mocked boundary without relying only on the HTTP status.
- Compare downloads exactly: Assert headers and binary content, using a digest for larger payloads.
- Keep layers distinct: Run fast MockMvc tests for HTTP behaviour and a small integration suite for real persistence.
Treat security and observability as part of the contract. Tests should cover unauthorised access, sanitised download filenames, and stable error responses. They should also reveal whether the application logs identifiers and validation failures without writing file contents or sensitive customer fields into CI output.
The practical takeaway is to make every file exchange reproducible: construct the exact multipart request, assert the metadata and bytes at the service boundary, and compare the downloaded payload byte for byte. A green MockMvc test should show that the HTTP contract is sound, not merely that a status code was returned.