Spring Boot
MockMvc
MockMvc tests Spring MVC controllers without starting a real HTTP server. It dispatches requests through the full DispatcherServlet filter chain — including security, validation, and serialisation — and provides a fluent assertion API for the response. Use @WebMvcTest for controller-slice tests or MockMvcBuilders.standaloneSetup() for unit-level controller tests. This entry covers setup, request building, response assertions, security testing, file upload, and JSON path assertions.
MockMvc Setup
@WebMvcTest loads the web layer only — controllers, filters, security, and argument resolvers. Declare @MockBean for every service dependency the controller injects. Alternatively, use MockMvcBuilders.standaloneSetup() to test a controller in complete isolation without any Spring context.
Java
// ── Option 1: @WebMvcTest — web slice (recommended) ─────────────────
@WebMvcTest(OrderController.class)
@ActiveProfiles("test")
class OrderControllerWebMvcTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
// Mock every dependency OrderController uses
@MockBean private OrderService orderService;
@MockBean private JwtService jwtService;
@MockBean private CustomUserDetailsService userDetailsService;
// ── Stub JwtService so @WebMvcTest security works ─────────────────
@BeforeEach
void stubSecurity() {
when(jwtService.extractSubject(anyString()))
.thenReturn("alice@example.com");
when(jwtService.isValid(anyString(), any()))
.thenReturn(true);
when(userDetailsService.loadUserByUsername(anyString()))
.thenReturn(buildPrincipal("alice@example.com", "USER"));
}
}
// ── Option 2: Standalone setup — no Spring context ────────────────────
class OrderControllerStandaloneTest {
private MockMvc mockMvc;
@BeforeEach
void setUp() {
OrderService orderService = mock(OrderService.class);
OrderController controller =
new OrderController(orderService);
mockMvc = MockMvcBuilders
.standaloneSetup(controller)
.setControllerAdvice(new GlobalExceptionHandler())
.addFilters(new CorrelationIdFilter())
.build();
}
}
// ── Option 3: Full context MockMvc ────────────────────────────────────
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class OrderFullContextTest {
@Autowired
private MockMvc mockMvc;
// Full context — real services and database
}Building Requests and Asserting Responses
MockMvc's perform() dispatches a request through the DispatcherServlet. Chain andExpect() calls to assert status, headers, content type, and body. andDo(print()) logs the full request and response for debugging. andReturn() gives access to the raw MvcResult for custom assertions.
Java
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockBean private ProductService productService;
@Test
@WithMockUser(roles = "USER")
void findAll_returns200_withPagedProducts()
throws Exception {
Page<ProductResponse> page = buildPage(
List.of(buildProductResponse(1L, "Widget"),
buildProductResponse(2L, "Gadget")));
when(productService.findAll(any(Pageable.class)))
.thenReturn(page);
mockMvc.perform(
get("/api/v1/products")
.param("page", "0")
.param("size", "20")
.param("sort", "name,asc")
.accept(MediaType.APPLICATION_JSON))
// ── Status ───────────────────────────────────────────────
.andExpect(status().isOk())
// ── Headers ──────────────────────────────────────────────
.andExpect(content().contentType(
MediaType.APPLICATION_JSON))
// ── JSON body — JSONPath ──────────────────────────────────
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.content.length()").value(2))
.andExpect(jsonPath("$.content[0].name")
.value("Widget"))
.andExpect(jsonPath("$.totalElements").value(2))
.andExpect(jsonPath("$.page").value(0))
// ── Debug — print request and response ────────────────────
.andDo(print());
}
@Test
@WithMockUser(roles = "USER")
void create_returns201_withLocationHeader()
throws Exception {
ProductResponse created = buildProductResponse(42L, "Widget");
when(productService.create(any()))
.thenReturn(created);
String requestBody = objectMapper.writeValueAsString(
new CreateProductRequest("Widget",
new BigDecimal("9.99"), "electronics"));
MvcResult result = mockMvc.perform(
post("/api/v1/products")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(header().string("Location",
containsString("/api/v1/products/42")))
.andExpect(jsonPath("$.id").value(42))
.andExpect(jsonPath("$.name").value("Widget"))
.andReturn();
// Access raw response if needed
String body = result.getResponse()
.getContentAsString();
ProductResponse response = objectMapper.readValue(
body, ProductResponse.class);
assertThat(response.id()).isEqualTo(42L);
}
@Test
@WithMockUser(roles = "USER")
void create_returns400_whenNameIsBlank()
throws Exception {
String invalid = objectMapper.writeValueAsString(
new CreateProductRequest("",
new BigDecimal("9.99"), "electronics"));
mockMvc.perform(
post("/api/v1/products")
.contentType(MediaType.APPLICATION_JSON)
.content(invalid))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.fieldErrors.name")
.isNotEmpty());
}
}Security Testing with MockMvc
@WebMvcTest loads the SecurityFilterChain. Test authentication and authorisation directly in MockMvc tests using @WithMockUser, @WithUserDetails, or by crafting a real JWT and sending it in the Authorization header. Test both the happy path and the security rejection scenarios.
Java
@WebMvcTest(OrderController.class)
class OrderControllerSecurityTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockBean private OrderService orderService;
@MockBean private JwtService jwtService;
@MockBean private CustomUserDetailsService userDetailsService;
// ── @WithMockUser — simplest, populates SecurityContext ───────────
@Test
@WithMockUser(username = "alice@example.com", roles = "USER")
void findById_returns200_forAuthenticatedUser()
throws Exception {
when(orderService.findById(eq(1L), any()))
.thenReturn(buildOrderResponse(1L));
mockMvc.perform(get("/api/v1/orders/1"))
.andExpect(status().isOk());
}
// ── Without authentication — 401 ─────────────────────────────────
@Test
void findById_returns401_withoutAuth() throws Exception {
mockMvc.perform(get("/api/v1/orders/1"))
.andExpect(status().isUnauthorized());
}
// ── Wrong role — 403 ─────────────────────────────────────────────
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_returns403_forRegularUser()
throws Exception {
mockMvc.perform(
delete("/api/v1/admin/users/1"))
.andExpect(status().isForbidden());
}
// ── @WithUserDetails — loads from UserDetailsService ─────────────
@Test
@WithUserDetails(value = "admin@example.com",
userDetailsServiceBeanName =
"customUserDetailsService")
void adminEndpoint_returns200_forAdminUser()
throws Exception {
mockMvc.perform(get("/api/v1/admin/stats"))
.andExpect(status().isOk());
}
// ── Real JWT in Authorization header ─────────────────────────────
@Test
void findById_returns200_withValidJwt()
throws Exception {
String email = "alice@example.com";
String jwt = "valid.jwt.token";
when(jwtService.extractSubject(jwt))
.thenReturn(email);
when(jwtService.isValid(eq(jwt), any()))
.thenReturn(true);
when(userDetailsService.loadUserByUsername(email))
.thenReturn(buildPrincipal(email, "USER"));
when(orderService.findById(eq(1L), any()))
.thenReturn(buildOrderResponse(1L));
mockMvc.perform(
get("/api/v1/orders/1")
.header(HttpHeaders.AUTHORIZATION,
"Bearer " + jwt))
.andExpect(status().isOk());
}
// ── Custom SecurityMockMvcRequestPostProcessor ─────────────────────
@Test
void withCustomPrincipal() throws Exception {
AppUser principal = buildAppUser(1L, "alice@example.com");
when(orderService.findById(eq(1L), any()))
.thenReturn(buildOrderResponse(1L));
mockMvc.perform(
get("/api/v1/orders/1")
.with(user(principal)))
.andExpect(status().isOk());
}
}File Upload and Multipart Requests
Test file upload endpoints with MockMultipartFile and multipart(). Verify the controller receives the file and metadata, delegates to the service, and returns the correct response. Test both valid uploads and rejections for oversized or wrong-type files.
Java
@WebMvcTest(FileController.class)
class FileControllerTest {
@Autowired private MockMvc mockMvc;
@MockBean private FileService fileService;
@Test
@WithMockUser(roles = "USER")
void upload_returns201_withFileMetadata()
throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", // param name
"document.pdf", // original filename
MediaType.APPLICATION_PDF_VALUE,// content type
"PDF content".getBytes() // content
);
FileUploadResponse response = new FileUploadResponse(
"file-uuid-123", "document.pdf",
"application/pdf", 12L,
"/api/v1/files/file-uuid-123",
Instant.now());
when(fileService.store(any(MultipartFile.class), any()))
.thenReturn(response);
mockMvc.perform(
multipart("/api/v1/files")
.file(file)
.param("description", "My document"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.fileId")
.value("file-uuid-123"))
.andExpect(jsonPath("$.filename")
.value("document.pdf"))
.andExpect(jsonPath("$.url").isNotEmpty());
}
@Test
@WithMockUser(roles = "USER")
void upload_withMultipleFiles() throws Exception {
MockMultipartFile file1 = new MockMultipartFile(
"files", "photo1.jpg",
MediaType.IMAGE_JPEG_VALUE,
"JPEG data".getBytes());
MockMultipartFile file2 = new MockMultipartFile(
"files", "photo2.jpg",
MediaType.IMAGE_JPEG_VALUE,
"JPEG data 2".getBytes());
when(fileService.store(any(), any()))
.thenReturn(buildFileResponse("uuid1"))
.thenReturn(buildFileResponse("uuid2"));
mockMvc.perform(
multipart("/api/v1/files/batch")
.file(file1)
.file(file2))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.length()").value(2));
}
@Test
@WithMockUser(roles = "USER")
void upload_returns400_forEmptyFile()
throws Exception {
MockMultipartFile empty = new MockMultipartFile(
"file", "empty.txt",
MediaType.TEXT_PLAIN_VALUE,
new byte[0]); // empty content
mockMvc.perform(
multipart("/api/v1/files").file(empty))
.andExpect(status().isBadRequest());
}
}JSON Path and Response Body Assertions
MockMvc supports JSONPath expressions for deep assertions into response bodies. For complex assertions, deserialise the response body using ObjectMapper and use AssertJ. Use ResultMatcher composition to build reusable assertion blocks.
Java
@WebMvcTest(UserController.class)
class UserControllerJsonTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockBean private UserService userService;
@Test
@WithMockUser(roles = "ADMIN")
void findAll_returnsPageWithCorrectStructure()
throws Exception {
when(userService.findAll(any()))
.thenReturn(buildUserPage());
mockMvc.perform(get("/api/v1/users"))
// ── Existence checks ──────────────────────────────────────
.andExpect(jsonPath("$.content").exists())
.andExpect(jsonPath("$.totalElements").exists())
// ── Type checks ───────────────────────────────────────────
.andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.totalPages").isNumber())
.andExpect(jsonPath("$.first").isBoolean())
// ── Value checks ──────────────────────────────────────────
.andExpect(jsonPath("$.content[0].id").value(1))
.andExpect(jsonPath("$.content[0].email")
.value("alice@example.com"))
.andExpect(jsonPath("$.content.length()").value(3))
// ── Contains checks ───────────────────────────────────────
.andExpect(jsonPath("$.content[0].roles[*]",
hasItem("USER")))
// ── Absence checks — sensitive fields not exposed ──────────
.andExpect(jsonPath("$.content[0].password")
.doesNotExist())
.andExpect(jsonPath("$.content[0].passwordHash")
.doesNotExist());
}
@Test
@WithMockUser(roles = "ADMIN")
void findById_responseDeserializesCorrectly()
throws Exception {
when(userService.findById(1L))
.thenReturn(buildUserResponse(1L));
String body = mockMvc.perform(
get("/api/v1/users/1"))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
// Deserialise and use AssertJ for fluent assertions
UserResponse response = objectMapper.readValue(
body, UserResponse.class);
assertThat(response.id()).isEqualTo(1L);
assertThat(response.email())
.isEqualTo("alice@example.com");
assertThat(response.roles())
.containsExactlyInAnyOrder("USER");
assertThat(response.createdAt()).isNotNull();
}
// ── Reusable ResultMatcher ─────────────────────────────────────────
static ResultMatcher isPageResponse() {
return ResultMatcher.matchAll(
jsonPath("$.content").isArray(),
jsonPath("$.page").isNumber(),
jsonPath("$.size").isNumber(),
jsonPath("$.totalElements").isNumber(),
jsonPath("$.totalPages").isNumber(),
jsonPath("$.first").isBoolean(),
jsonPath("$.last").isBoolean()
);
}
@Test
@WithMockUser
void findAll_hasPageStructure() throws Exception {
when(userService.findAll(any())).thenReturn(buildUserPage());
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isOk())
.andExpect(isPageResponse()); // reusable matcher
}
}