Writing tests for Java servlets with Spring mock objects
Writing tests for Java servlets with MockHttpServletRequest and MockHttpServletResponse brings the HTTP contract into a fast, deterministic unit test. The request mock supplies parameters, headers, cookies, attributes and a body; the response mock captures the status, content type, output and other observable results. A test can therefore exercise servlet branches without starting Tomcat, Jetty or another servlet container. This is especially useful for code that parses input, delegates to domain services or chooses an error response.
Both classes come from Spring Framework’s spring-test module. They implement Servlet API contracts, not a complete web server, so they do not prove that deployment descriptors, filters, TLS or container-managed sessions behave correctly. Their purpose is to isolate the servlet’s own decisions while keeping the feedback loop short. Use container-level tests alongside them when those platform behaviours are part of the risk.
For teams in Sydney and Melbourne, that speed fits common Java delivery pipelines built with Gradle or Maven and hosted through GitHub, GitLab or Bitbucket. Australian market expectations also make careful boundary testing valuable: where a servlet handles personal information, assess the Privacy Act 1988 and Australian Privacy Principles, while security controls can be aligned with ACSC guidance and the ASD Essential Eight. The legal and assurance context does not change the testing mechanics, but it does sharpen which data and failure paths deserve coverage.
Preparing a servlet test harness
Place the test beside the servlet and construct MockHttpServletRequest and MockHttpServletResponse directly. Their package is org.springframework.mock.web. Spring Framework 6 uses the Jakarta Servlet namespace, while earlier versions use the older javax.servlet API, so the test dependency must match the application rather than mixing APIs.
A typical JUnit test creates both mocks, supplies only the input required by the scenario and calls the servlet:
@Test
void rejectsAnInvalidCustomerId() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setMethod("GET");
request.setRequestURI("/customers/not-a-number");
MockHttpServletResponse response = new MockHttpServletResponse();
servlet.service(request, response);
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentType()).startsWith("application/problem+json");
assertThat(response.getContentAsString()).contains("customerId");
}
Keep the servlet itself real and mock only collaborators such as repositories, clients or validation services. Assert the resulting HTTP behaviour after the call rather than checking every internal interaction. Over-specified interaction tests become brittle when implementation details change without altering the REST API contract.
Exercising request handling
Build each request explicitly with the values relevant to the branch under examination. setMethod, setRequestURI, setContextPath and setServletPath describe the route, while setParameter and addParameter cover query-string and form-style data. Additional properties include dispatcher type, remote address, secure status, locale and server details. These setters let a test reproduce unusual routing conditions without deploying a WAR file.
A request body is supplied as bytes through setContent, then read from getInputStream or getReader. Set the content type and character encoding before invoking the servlet when the production path depends on them. Headers and cookies can be added independently, allowing tests to distinguish a missing bearer token from an expired one or to verify that request attributes created by a filter are consumed correctly.
Use generated but predictable test data rather than random values that make failures difficult to reproduce. A fixed customer ID, timestamp or payload is often more valuable than broad data variety. Explicit setup also prevents one test’s shared mock state from leaking into another and keeps each assertion connected to a known cause.
Inspecting response behaviour
Status, headers, cookies and the response body form the observable result. MockHttpServletResponse records the status code, error message, content type and character encoding. It also exposes headers, cookies, redirected locations and sent-error information, making it possible to verify a complete response rather than checking only its JSON payload.
Call getContentAsString after the servlet has finished writing. This captures text written through getWriter, while getContentAsByteArray supports binary output. Tests should also confirm that the declared content type matches the payload, such as application/json for an API response or text/csv for an export. A correct body with the wrong media type can still break clients and monitoring systems.
Treat output stream and encoding rules as part of the contract. Servlet containers generally prohibit mixing writer and output-stream access, and response commitment affects whether later headers can change. Check isCommitted when testing download endpoints, large responses or failure handling after output begins. These assertions catch subtle differences that a basic status-code test misses.
Covering sessions, errors and asynchronous work
Request sessions are easy to isolate with MockHttpServletRequest. Calling getSession(true) creates a mock session in which attributes can be added and read. Tests can then verify login state, selected customer context or form data without relying on a shared container session. This is particularly useful for testing access decisions separately from the authentication filter that would create the session in production.
Exercise error paths deliberately. A servlet may throw an exception, call sendError or return a structured problem response, and each mechanism has different observable results. Assert the status, payload and any correlation identifier consistently with the service’s error contract. Unexpected exceptions should remain visible in unit tests rather than being swallowed merely to make the test pass.
For asynchronous request processing, mocking is appropriate for deciding what should happen after deferred work succeeds or fails. It does not reproduce a container’s thread handoff, executor capacity or timeout behaviour under load. Cover those mechanics with Spring’s asynchronous test support or an embedded-server integration test. This separation keeps fast backend tests focused on logic while reserving performance testing for realistic concurrency conditions.
Protecting data and security expectations
Security-related servlet tests should cover hostile inputs as ordinary test cases. Include malformed identifiers, path traversal sequences, unexpected Unicode, duplicate parameters and oversized headers. Verify that output is encoded for its destination and that user-controlled values cannot inject additional response headers. Authentication and authorisation failures should also produce the documented status and response body.
For organisations covered by the Privacy Act 1988, the Australian Privacy Principles influence what servlet logs, temporary files and error messages may contain. Tests should prove that identifiers are accepted only for permitted purposes, sensitive fields are omitted from responses and personal data is not copied into diagnostic text. These checks support broader privacy obligations without turning every unit test into a legal review.
Security logging and operational visibility deserve assertions too. Confirm that a correlation ID reaches the correct response field and that credentials or personal records are absent from messages. This aligns practical verification with Australian Cyber Security Centre guidance and the ASD Essential Eight, while producing evidence that can support continuous delivery pipelines and later audits.
Choosing the right test boundary
A direct servlet unit test is the quickest way to cover branching, validation, delegation and error mapping. It provides precise failure messages and has few environmental dependencies. Its limitation is equally important: filters, servlet mappings, real networking and container configuration remain outside the exercised boundary.
Spring MockMvc sits between a unit test and a deployed application. It exercises Spring MVC routing, argument resolution, filters and response serialisation without requiring an external port. An embedded-server integration test goes further, using real HTTP behaviour over a socket. The appropriate boundary depends on whether the risk lies in servlet logic, framework integration or container behaviour.
| Approach | Exercises | Best fit | Main limitation |
|---|---|---|---|
| Direct servlet unit test | Servlet branches, collaborators and HTTP output | Fast validation of domain-specific behaviour | Filters and container features are absent |
| Spring MockMvc | Spring MVC routing, binding and filters | Controller and REST API integration tests | Not identical to a deployed container |
| Embedded server test | Real HTTP handling and network transport | Headers, streaming and deployment confidence | Slower and more environment-dependent |
A practical servlet testing checklist
A useful final pass examines the request contract independently from the response contract. This makes missing setup visible and keeps failures understandable.
Request-focused checks
- Does each required parameter, header, cookie or path value reach the servlet unchanged?
- Are missing, malformed, duplicated and oversized inputs mapped to the intended response?
- Does the request body decode correctly for the declared character encoding?
- Are session, locale, remote address and request attributes set only when relevant?
A second pass confirms what the client receives, including metadata that is easy to overlook in payload-only assertions.
Response-focused checks
- Is the status code correct for success, validation failure, authorisation failure and server error?
- Do headers and cookies contain the expected values without unsafe or duplicate entries?
- Does the body match the declared content type and character encoding?
- Are redirects, sent errors and committed-response behaviour handled as designed?
The durable principle is to keep most servlet tests at the unit boundary, assert the HTTP contract rather than internal calls, and reserve embedded-container or end-to-end checks for behaviour that genuinely depends on real HTTP infrastructure.