Week 7 Day 3 — Web Layer Testing with @WebMvcTest and MockMvc
Goal
Today I want to understand how to test the web/controller layer in Spring Boot.
Main questions:
- What is a web layer test?
- What does
@WebMvcTestload? - What does
@WebMvcTestnot load? - What is MockMvc?
- How do I test GET endpoints?
- How do I test POST endpoints with JSON?
- How do I test request validation?
- How do I test JSON response fields?
- How do I test exception handling?
- How do I test controller security?
- What is the difference between
@WebMvcTestand@SpringBootTest? - What are common exam traps?
1. Quick Review from Week 7 Day 2
In Day 2, I learned:
- Unit tests do not start Spring.
- JUnit runs tests.
- AssertJ checks results fluently.
- Mockito mocks dependencies.
- Stub means fake return.
- Verify means check interaction.
ArgumentCaptorcaptures arguments passed to mocks.- Unit tests are good for service business logic.
- Unit tests do not prove Spring MVC, JSON conversion, validation, or security filters.
Memory sentence:
If the test does not need Spring, do not start Spring.
Today I test the part that does need Spring MVC:
Controller + request mapping + JSON + validation + HTTP response.
2. What Is a Web Layer Test?
A web layer test checks the controller layer.
It answers questions like:
Does this URL call the right controller method?
Does the controller read path variables correctly?
Does it read query parameters correctly?
Does it deserialize JSON request body correctly?
Does validation return 400 for invalid input?
Does the response have the correct HTTP status?
Does the response contain the correct JSON fields?
Does @ControllerAdvice return the right error response?
Does Spring Security block or allow this request?
Memory sentence:
Web layer tests prove HTTP behavior.
3. What Is @WebMvcTest?
@WebMvcTest loads a focused Spring MVC test slice.
It usually includes:
controllers
controller advice
JSON conversion
validation support
MockMvc
Spring MVC infrastructure
security filter support by default
It usually does not load:
normal service beans
repositories
database
full application context
external clients
scheduled jobs
all auto-configuration
Memory sentence:
@WebMvcTesttests the MVC slice, not the whole app.
4. Why Use @WebMvcTest?
Compared with @SpringBootTest, @WebMvcTest is:
faster
more focused
better for controller behavior
easier to isolate HTTP behavior
Example:
@WebMvcTest(TaskController.class)
class TaskControllerTest {
}
This means:
Load MVC support for TaskController only.
Memory sentence:
Use
@WebMvcTestwhen I want to test controller behavior without the full app.
5. What Is MockMvc?
MockMvc lets me perform HTTP-like requests without starting a real server.
Example:
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk());
MockMvc goes through Spring MVC infrastructure.
It can test:
request mapping
path variables
query parameters
request body
JSON conversion
validation
controller advice
response status
response body
headers
security filters
Memory sentence:
MockMvc tests MVC requests without a real HTTP server.
6. @WebMvcTest Basic Structure
Controller:
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping("/{id}")
public TaskDto findById(@PathVariable Long id) {
return taskService.findById(id);
}
}
Test:
@WebMvcTest(TaskController.class)
class TaskControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private TaskService taskService;
@Test
void returnsTaskById() throws Exception {
when(taskService.findById(1L))
.thenReturn(new TaskDto(1L, "Learn WebMvcTest", "OPEN"));
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("Learn WebMvcTest"))
.andExpect(jsonPath("$.status").value("OPEN"));
}
}
Important:
Controller is real.
TaskService is mocked.
HTTP behavior is tested.
Memory sentence:
In
@WebMvcTest, mock the controller collaborators.
7. @MockitoBean vs @MockBean
In modern Spring tests, you may see:
@MockitoBean
private TaskService taskService;
In many older Spring Boot projects, you may see:
@MockBean
private TaskService taskService;
They serve the same learning purpose here:
replace a Spring bean with a Mockito mock in the test context
For this book, I will use:
@MockitoBean
If your project uses an older Spring Boot version, use:
@MockBean
Memory sentence:
Use a Mockito Spring bean when the controller needs a service dependency.
8. Example DTOs
Response DTO:
public record TaskDto(
Long id,
String title,
String status
) {
}
Create request:
public record CreateTaskRequest(
@NotBlank(message = "Title must not be blank")
@Size(max = 100, message = "Title must be at most 100 characters")
String title
) {
}
Create response:
public record CreateTaskResponse(
Long id,
String title,
String status
) {
}
Memory sentence:
Web tests usually work with request and response DTOs.
9. Controller with GET and POST
@RestController
@RequestMapping("/api/tasks")
public class TaskController {
private final TaskService taskService;
public TaskController(TaskService taskService) {
this.taskService = taskService;
}
@GetMapping("/{id}")
public TaskDto findById(@PathVariable Long id) {
return taskService.findById(id);
}
@PostMapping
public ResponseEntity<CreateTaskResponse> create(
@Valid @RequestBody CreateTaskRequest request
) {
CreateTaskResponse response = taskService.create(request);
URI location = URI.create("/api/tasks/" + response.id());
return ResponseEntity
.created(location)
.body(response);
}
}
This controller handles:
GET /api/tasks/{id}
POST /api/tasks
10. Testing GET with Path Variable
@Test
void returnsTaskById() throws Exception {
when(taskService.findById(1L))
.thenReturn(new TaskDto(1L, "Task A", "OPEN"));
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("Task A"))
.andExpect(jsonPath("$.status").value("OPEN"));
verify(taskService).findById(1L);
}
This tests:
path variable is read correctly
service is called with id 1
response status is 200
response JSON is correct
Memory sentence:
Use MockMvc GET tests to verify path variables and JSON response.
11. Testing Query Parameters
Controller:
@GetMapping
public List<TaskDto> search(
@RequestParam String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return taskService.search(status, page, size);
}
Test:
@Test
void searchesTasksByStatus() throws Exception {
when(taskService.search("OPEN", 0, 20))
.thenReturn(List.of(
new TaskDto(1L, "Task A", "OPEN"),
new TaskDto(2L, "Task B", "OPEN")
));
mockMvc.perform(get("/api/tasks")
.param("status", "OPEN")
.param("page", "0")
.param("size", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$[0].title").value("Task A"))
.andExpect(jsonPath("$[1].title").value("Task B"));
verify(taskService).search("OPEN", 0, 20);
}
Memory sentence:
Use
.param(...)for query parameters.
12. Testing Default Query Parameters
Controller:
@GetMapping
public List<TaskDto> search(
@RequestParam String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return taskService.search(status, page, size);
}
Test:
@Test
void usesDefaultPagination() throws Exception {
when(taskService.search("OPEN", 0, 20))
.thenReturn(List.of());
mockMvc.perform(get("/api/tasks")
.param("status", "OPEN"))
.andExpect(status().isOk());
verify(taskService).search("OPEN", 0, 20);
}
This proves:
page defaults to 0
size defaults to 20
Memory sentence:
Web tests can verify default request parameter values.
13. Testing POST with JSON
Test:
@Test
void createsTask() throws Exception {
when(taskService.create(any(CreateTaskRequest.class)))
.thenReturn(new CreateTaskResponse(1L, "Task A", "OPEN"));
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": "Task A"
}
"""))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/tasks/1"))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("Task A"))
.andExpect(jsonPath("$.status").value("OPEN"));
}
This tests:
JSON request body is accepted
controller returns 201 Created
Location header is set
JSON response is correct
Memory sentence:
Use
.contentType(APPLICATION_JSON)and.content(...)for JSON request bodies.
14. Using ObjectMapper in Tests
Instead of writing JSON manually, I can use ObjectMapper.
@Autowired
private ObjectMapper objectMapper;
Test:
@Test
void createsTaskUsingObjectMapper() throws Exception {
when(taskService.create(any(CreateTaskRequest.class)))
.thenReturn(new CreateTaskResponse(1L, "Task A", "OPEN"));
CreateTaskRequest request = new CreateTaskRequest("Task A");
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value("Task A"));
}
Benefits:
less manual JSON typing
uses same Jackson configuration
safer when DTO changes
Memory sentence:
Use ObjectMapper when JSON gets complex.
15. Testing Request Body Passed to Service
Sometimes I want to verify what request object reached the service.
Use ArgumentCaptor.
@Test
void passesRequestBodyToService() throws Exception {
when(taskService.create(any(CreateTaskRequest.class)))
.thenReturn(new CreateTaskResponse(1L, "Task A", "OPEN"));
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": "Task A"
}
"""))
.andExpect(status().isCreated());
ArgumentCaptor<CreateTaskRequest> captor =
ArgumentCaptor.forClass(CreateTaskRequest.class);
verify(taskService).create(captor.capture());
CreateTaskRequest captured = captor.getValue();
assertThat(captured.title()).isEqualTo("Task A");
}
Memory sentence:
Use ArgumentCaptor when I need to inspect data passed from controller to service.
16. Request Validation
Request DTO:
public record CreateTaskRequest(
@NotBlank(message = "Title must not be blank")
@Size(max = 100, message = "Title must be at most 100 characters")
String title
) {
}
Controller:
@PostMapping
public ResponseEntity<CreateTaskResponse> create(
@Valid @RequestBody CreateTaskRequest request
) {
CreateTaskResponse response = taskService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
Important:
@Valid triggers validation on the request body.
Without @Valid, validation annotations on DTO may not run.
Memory sentence:
Validation needs
@Validon the controller parameter.
17. Testing Validation Error
Test blank title:
@Test
void returnsBadRequestWhenTitleIsBlank() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": ""
}
"""))
.andExpect(status().isBadRequest());
verify(taskService, never()).create(any());
}
This proves:
invalid request returns 400
service is not called
Memory sentence:
Invalid request should return 400 before service logic runs.
18. Testing Validation Error Body
To test a clean error response, create controller advice.
Error DTO:
public record FieldErrorDto(
String field,
String message
) {
}
public record ValidationErrorResponse(
String code,
List<FieldErrorDto> errors
) {
}
Controller advice:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidation(
MethodArgumentNotValidException exception
) {
List<FieldErrorDto> errors = exception.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldErrorDto(
error.getField(),
error.getDefaultMessage()
))
.toList();
return ResponseEntity.badRequest()
.body(new ValidationErrorResponse("VALIDATION_ERROR", errors));
}
}
Test:
@Test
void returnsValidationErrorBody() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": ""
}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
.andExpect(jsonPath("$.errors[0].field").value("title"))
.andExpect(jsonPath("$.errors[0].message").value("Title must not be blank"));
}
Memory sentence:
Web tests should verify the error format clients depend on.
19. Testing Missing Request Body
Controller expects:
@Valid @RequestBody CreateTaskRequest request
Test:
@Test
void returnsBadRequestWhenBodyIsMissing() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isBadRequest());
verify(taskService, never()).create(any());
}
Memory sentence:
Missing JSON body should usually return 400.
20. Testing Invalid JSON
Test malformed JSON:
@Test
void returnsBadRequestWhenJsonIsInvalid() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": "Task A"
"""))
.andExpect(status().isBadRequest());
verify(taskService, never()).create(any());
}
This tests:
Jackson cannot parse request body
controller method is not successfully invoked
service is not called
Memory sentence:
Invalid JSON should fail before service logic.
21. Testing Wrong Content Type
If endpoint consumes JSON, wrong content type may fail.
Test:
@Test
void returnsUnsupportedMediaTypeForTextPlain() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.TEXT_PLAIN)
.content("title=Task A"))
.andExpect(status().isUnsupportedMediaType());
}
Expected:
415 Unsupported Media Type
depending on controller configuration and converters.
Memory sentence:
Web tests can verify content type behavior.
22. Testing Response Headers
Controller:
URI location = URI.create("/api/tasks/" + response.id());
return ResponseEntity
.created(location)
.body(response);
Test:
.andExpect(header().string("Location", "/api/tasks/1"))
Full example:
@Test
void createReturnsLocationHeader() throws Exception {
when(taskService.create(any(CreateTaskRequest.class)))
.thenReturn(new CreateTaskResponse(1L, "Task A", "OPEN"));
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/tasks/1"));
}
Memory sentence:
Test headers when clients rely on them.
23. Testing No Content Response
Controller:
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
taskService.delete(id);
return ResponseEntity.noContent().build();
}
Test:
@Test
void deletesTask() throws Exception {
mockMvc.perform(delete("/api/tasks/1"))
.andExpect(status().isNoContent())
.andExpect(content().string(""));
verify(taskService).delete(1L);
}
Memory sentence:
204 No Content should not return a response body.
24. Testing Exception Handling
Service throws:
throw new ResourceNotFoundException("Task", 99L);
Controller advice:
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleNotFound(
ResourceNotFoundException exception
) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ApiErrorResponse(
"NOT_FOUND",
exception.getMessage()
));
}
}
Error DTO:
public record ApiErrorResponse(
String code,
String message
) {
}
Test:
@Test
void returnsNotFoundWhenTaskDoesNotExist() throws Exception {
when(taskService.findById(99L))
.thenThrow(new ResourceNotFoundException("Task", 99L));
mockMvc.perform(get("/api/tasks/99"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("NOT_FOUND"))
.andExpect(jsonPath("$.message").value("Task not found: 99"));
}
Memory sentence:
Web tests should verify controller advice error responses.
25. Testing Business Exceptions
Exception:
public class BusinessConflictException extends RuntimeException {
public BusinessConflictException(String message) {
super(message);
}
}
Handler:
@ExceptionHandler(BusinessConflictException.class)
public ResponseEntity<ApiErrorResponse> handleConflict(
BusinessConflictException exception
) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ApiErrorResponse(
"BUSINESS_CONFLICT",
exception.getMessage()
));
}
Test:
@Test
void returnsConflictForBusinessRuleViolation() throws Exception {
doThrow(new BusinessConflictException("Task is already done"))
.when(taskService)
.delete(1L);
mockMvc.perform(delete("/api/tasks/1"))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value("BUSINESS_CONFLICT"))
.andExpect(jsonPath("$.message").value("Task is already done"));
}
Memory sentence:
Controller advice tests protect the API error contract.
26. Testing Arrays
Response:
[
{
"id": 1,
"title": "Task A"
},
{
"id": 2,
"title": "Task B"
}
]
Test:
@Test
void returnsTaskList() throws Exception {
when(taskService.search("OPEN", 0, 20))
.thenReturn(List.of(
new TaskDto(1L, "Task A", "OPEN"),
new TaskDto(2L, "Task B", "OPEN")
));
mockMvc.perform(get("/api/tasks")
.param("status", "OPEN"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$").isNotEmpty())
.andExpect(jsonPath("$[0].id").value(1))
.andExpect(jsonPath("$[0].title").value("Task A"))
.andExpect(jsonPath("$[1].id").value(2))
.andExpect(jsonPath("$[1].title").value("Task B"));
}
Memory sentence:
Use
$[0],$[1]for JSON arrays.
27. Testing Empty Arrays
@Test
void returnsEmptyListWhenNoTasksFound() throws Exception {
when(taskService.search("DONE", 0, 20))
.thenReturn(List.of());
mockMvc.perform(get("/api/tasks")
.param("status", "DONE"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray())
.andExpect(jsonPath("$").isEmpty());
}
Memory sentence:
Empty list responses should also be tested.
28. Testing Date/Time JSON
DTO:
public record TaskDto(
Long id,
String title,
String status,
LocalDate dueDate
) {
}
Test:
@Test
void returnsDueDate() throws Exception {
when(taskService.findById(1L))
.thenReturn(new TaskDto(
1L,
"Task A",
"OPEN",
LocalDate.of(2026, 7, 7)
));
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.dueDate").value("2026-07-07"));
}
Memory sentence:
Test date/time JSON when API clients depend on the format.
29. Testing Enum JSON
Enum:
public enum TaskStatus {
OPEN,
DONE
}
DTO:
public record TaskDto(
Long id,
String title,
TaskStatus status
) {
}
Test:
@Test
void returnsEnumAsString() throws Exception {
when(taskService.findById(1L))
.thenReturn(new TaskDto(1L, "Task A", TaskStatus.OPEN));
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("OPEN"));
}
Memory sentence:
Web tests can protect enum JSON format.
30. Testing Security with @WebMvcTest
@WebMvcTest often includes Spring Security.
If endpoint requires authentication, this test may return 401:
@Test
void anonymousCannotAccessTasks() throws Exception {
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isUnauthorized());
}
For authenticated user:
@Test
@WithMockUser(authorities = "TASK_READ")
void userWithTaskReadCanAccessTask() throws Exception {
when(taskService.findById(1L))
.thenReturn(new TaskDto(1L, "Task A", "OPEN"));
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk());
}
Memory sentence:
Web layer tests can include security behavior.
31. Testing Role-Based Access
Security rule:
.requestMatchers("/api/admin/**").hasRole("ADMIN")
Test denied:
@Test
@WithMockUser(roles = "USER")
void userCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
Test allowed:
@Test
@WithMockUser(roles = "ADMIN")
void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
Memory sentence:
Test both allowed and denied security cases.
32. Testing CSRF in Web Layer Tests
If CSRF is enabled, unsafe methods need CSRF token.
Unsafe methods:
POST
PUT
PATCH
DELETE
Test with CSRF:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void createsTaskWithCsrf() throws Exception {
when(taskService.create(any(CreateTaskRequest.class)))
.thenReturn(new CreateTaskResponse(1L, "Task A", "OPEN"));
mockMvc.perform(post("/api/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
}
Test missing CSRF:
@Test
@WithMockUser(authorities = "TASK_WRITE")
void createWithoutCsrfIsForbidden() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isForbidden());
}
Memory sentence:
permitAlland correct authority do not automatically bypass CSRF.
33. Disabling Security Filters in Controller Tests
Sometimes I want to test controller behavior only, not security.
Possible option:
@WebMvcTest(TaskController.class)
@AutoConfigureMockMvc(addFilters = false)
class TaskControllerTest {
}
This disables filters, including security filters.
Use carefully.
Good for:
pure controller mapping tests
when security is tested separately
Risk:
test may pass even though real request is blocked by security
Memory sentence:
Disabling filters makes controller tests simpler but less realistic.
34. Importing Security Config
If I want a specific security configuration in @WebMvcTest, I can import it.
@WebMvcTest(TaskController.class)
@Import(SecurityConfig.class)
class TaskControllerSecurityTest {
}
This is useful when:
controller test should use my application security rules
security beans are not automatically loaded
custom security config is needed
Memory sentence:
Import security config when the web slice needs your real security rules.
35. Testing Custom Principal
Controller:
@GetMapping("/api/me")
public MeDto me(@AuthenticationPrincipal AppUserPrincipal principal) {
return new MeDto(principal.getId(), principal.getUsername());
}
Test:
@Test
void returnsCurrentUser() throws Exception {
AppUserPrincipal principal = new AppUserPrincipal(
10L,
5L,
"steve@example.com",
List.of(new SimpleGrantedAuthority("ROLE_USER")),
true
);
mockMvc.perform(get("/api/me")
.with(user(principal)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(10))
.andExpect(jsonPath("$.email").value("steve@example.com"));
}
Memory sentence:
Use
.with(user(customPrincipal))when the controller expects a custom principal.
36. Testing JWT in Web Layer
If controller/security uses JWT resource server, I can mock JWT.
@Test
void userWithJwtCanReadTasks() throws Exception {
when(taskService.findById(1L))
.thenReturn(new TaskDto(1L, "Task A", "OPEN"));
mockMvc.perform(get("/api/tasks/1")
.with(jwt().authorities(
new SimpleGrantedAuthority("SCOPE_task:read")
)))
.andExpect(status().isOk());
}
Test denied:
@Test
void jwtWithoutReadScopeIsForbidden() throws Exception {
mockMvc.perform(get("/api/tasks/1")
.with(jwt().authorities(
new SimpleGrantedAuthority("SCOPE_task:write")
)))
.andExpect(status().isForbidden());
}
Memory sentence:
Use
jwt()for web tests of resource-server authorization.
37. Testing Multipart Upload
Controller:
@PostMapping("/api/documents")
public DocumentDto upload(@RequestParam MultipartFile file) {
return documentService.upload(file);
}
Test:
@Test
@WithMockUser(authorities = "DOCUMENT_UPLOAD")
void uploadsDocument() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"test.pdf",
"application/pdf",
"PDF content".getBytes(StandardCharsets.UTF_8)
);
when(documentService.upload(any(MultipartFile.class)))
.thenReturn(new DocumentDto(1L, "test.pdf"));
mockMvc.perform(multipart("/api/documents")
.file(file)
.with(csrf()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.filename").value("test.pdf"));
}
Memory sentence:
MockMvc can test multipart requests too.
38. Testing Headers
Controller:
@GetMapping("/api/tasks")
public List<TaskDto> list(@RequestHeader("X-Tenant-Id") Long tenantId) {
return taskService.listForTenant(tenantId);
}
Test:
@Test
void readsTenantHeader() throws Exception {
when(taskService.listForTenant(5L))
.thenReturn(List.of());
mockMvc.perform(get("/api/tasks")
.header("X-Tenant-Id", "5"))
.andExpect(status().isOk());
verify(taskService).listForTenant(5L);
}
Memory sentence:
Use
.header(...)to test request headers.
39. Testing Missing Header
@Test
void returnsBadRequestWhenTenantHeaderMissing() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isBadRequest());
verify(taskService, never()).listForTenant(any());
}
Memory sentence:
Missing required headers usually return 400.
40. Testing Cookies
Controller:
@GetMapping("/api/preferences")
public PreferenceDto preferences(@CookieValue("theme") String theme) {
return new PreferenceDto(theme);
}
Test:
@Test
void readsCookie() throws Exception {
mockMvc.perform(get("/api/preferences")
.cookie(new Cookie("theme", "dark")))
.andExpect(status().isOk())
.andExpect(jsonPath("$.theme").value("dark"));
}
Memory sentence:
MockMvc can send cookies with
.cookie(...).
41. @WebMvcTest vs @SpringBootTest
| Topic | @WebMvcTest | @SpringBootTest |
|---|---|---|
| Scope | MVC slice | full application context |
| Speed | faster | slower |
| Service | usually mocked | real unless mocked |
| Repository | not loaded | loaded if in context |
| Database | no | possible |
| Best for | controller behavior | full flow/integration |
| MockMvc | auto-configured | use @AutoConfigureMockMvc |
Memory sentence:
@WebMvcTestis focused;@SpringBootTestis complete.
42. When to Use @WebMvcTest
Use @WebMvcTest for:
request mapping
path variables
query parameters
request headers
request body JSON
validation
response status
response JSON
controller advice
security at controller level
Do not use it to prove:
service business logic
repository queries
database mapping
transaction behavior
full application flow
Memory sentence:
Use web tests for HTTP contract, not service logic.
43. What Not to Over-Test
Avoid testing Spring itself.
Not useful:
Does @GetMapping work at all?
Does Jackson generally serialize JSON?
Does MockMvc work?
Useful:
Does my endpoint URL work?
Does my request DTO validate correctly?
Does my error response match my API contract?
Does my controller call service with correct data?
Does my security rule block wrong users?
Memory sentence:
Test your API contract, not the framework in general.
44. Common Web Test Mistakes
Mistake 1:
Forgetting to mock service dependency.
Mistake 2:
Testing service business logic in @WebMvcTest.
Mistake 3:
Forgetting @Valid in controller and wondering why validation test fails.
Mistake 4:
Forgetting contentType(APPLICATION_JSON).
Mistake 5:
Expecting 201 but controller returns 200.
Mistake 6:
Forgetting CSRF for POST/PUT/PATCH/DELETE when CSRF is enabled.
Mistake 7:
Disabling security filters and thinking security is tested.
Mistake 8:
Using @SpringBootTest for every controller test.
Mistake 9:
Over-verifying every service interaction.
Mistake 10:
Not testing error responses.
45. Common Exam Traps
Trap 1
@WebMvcTest is a slice test.
Trap 2
@WebMvcTest does not load the full application context.
Trap 3
Services are usually mocked in @WebMvcTest.
Trap 4
MockMvc performs requests without a real server.
Trap 5
@RequestBody JSON is deserialized through HTTP message converters.
Trap 6
Validation on request body needs @Valid.
Trap 7
Invalid validation usually returns 400.
Trap 8
Malformed JSON usually returns 400.
Trap 9
Wrong content type can return 415.
Trap 10
@RestControllerAdvice can be tested with @WebMvcTest.
Trap 11
Security filters may be active in @WebMvcTest.
Trap 12
POST tests may need CSRF if CSRF is enabled.
Trap 13
@AutoConfigureMockMvc(addFilters = false) disables security filters.
Trap 14
Disabling filters means security is not tested.
Trap 15
Use @SpringBootTest when I need real service/repository/full flow.
46. Real Exam Question: @WebMvcTest
Question:
What is @WebMvcTest used for?
Answer:
@WebMvcTest is used to test the Spring MVC web layer, especially controllers, request mapping, validation, JSON serialization/deserialization, controller advice, and MockMvc behavior.
47. Real Exam Question: MockMvc
Question:
What is MockMvc?
Answer:
MockMvc is a Spring MVC testing tool that performs HTTP-like requests through Spring MVC infrastructure without starting a real web server.
48. Real Exam Question: Service in @WebMvcTest
Question:
Are service beans normally loaded in @WebMvcTest?
Answer:
No. @WebMvcTest focuses on the web layer, so service dependencies are usually mocked with @MockitoBean or similar test mock annotations.
49. Real Exam Question: Validation
Question:
What annotation triggers validation on a request body?
Answer:
@Valid on the @RequestBody controller parameter.
50. Real Exam Question: Invalid Request
Question:
What HTTP status is commonly returned for validation errors?
Answer:
400 Bad Request.
51. Real Exam Question: Controller Advice
Question:
Can @RestControllerAdvice be tested in web layer tests?
Answer:
Yes. @WebMvcTest can test controller advice and verify error responses.
52. Real Exam Question: CSRF in Web Tests
Question:
What might a POST request need in a MockMvc test if CSRF is enabled?
Answer:
It needs .with(csrf()).
53. Interview Answer
Question:
When would you use
@WebMvcTest?
Good answer:
I use @WebMvcTest when I want to test the controller layer without starting the full application. It is good for request mapping, path variables, query parameters, request body JSON, validation, response status, response JSON, and controller exception handling. I mock service dependencies because I want the test to focus on HTTP behavior, not service or database logic.
54. Interview Answer
Question:
What is the difference between
@WebMvcTestand@SpringBootTest?
Good answer:
@WebMvcTest loads only the MVC slice, so it is faster and focused on controllers. Services and repositories are usually not loaded and must be mocked. @SpringBootTest loads the full application context and is used when I need multiple layers working together, such as controller, service, repository, security, and database integration.
55. Interview Answer
Question:
How do you test validation in a controller?
Good answer:
I create a request DTO with Bean Validation annotations, annotate the controller parameter with @Valid, then use MockMvc to send invalid JSON. I expect 400 Bad Request and, if the API has a custom error format, I assert the JSON error response. I also verify that the service was not called.
56. Interview Answer
Question:
How do you test controller exception handling?
Good answer:
I mock the service to throw a specific exception, such as ResourceNotFoundException. Then I perform the request with MockMvc and assert the HTTP status and JSON error body returned by @RestControllerAdvice.
57. Interview Answer
Question:
How do security filters affect
@WebMvcTest?
Good answer:
@WebMvcTest can include Spring Security filters, so protected endpoints may return 401 or 403 in tests. I can use @WithMockUser, user(), jwt(), or csrf() from Spring Security test support. If I disable filters with @AutoConfigureMockMvc(addFilters = false), I am no longer testing security behavior.
58. Tiny Code Practice
Controller:
@PostMapping("/api/tasks")
public ResponseEntity<CreateTaskResponse> create(
@Valid @RequestBody CreateTaskRequest request
) {
CreateTaskResponse response = taskService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
Write tests for:
valid request -> 201
blank title -> 400
invalid JSON -> 400
Possible answer:
@Test
void createsTask() throws Exception {
when(taskService.create(any(CreateTaskRequest.class)))
.thenReturn(new CreateTaskResponse(1L, "Task A", "OPEN"));
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("Task A"));
}
@Test
void returnsBadRequestWhenTitleIsBlank() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":""}
"""))
.andExpect(status().isBadRequest());
verify(taskService, never()).create(any());
}
@Test
void returnsBadRequestWhenJsonIsInvalid() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"title":"Task A"
"""))
.andExpect(status().isBadRequest());
verify(taskService, never()).create(any());
}
59. Tiny Bug Practice 1
Problem:
@WebMvcTest(TaskController.class)
class TaskControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void returnsTask() throws Exception {
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk());
}
}
The test fails because TaskService bean is missing.
Question:
What is missing?
Answer:
The service dependency should be mocked.
@MockitoBean
private TaskService taskService;
Older projects may use:
@MockBean
private TaskService taskService;
60. Tiny Bug Practice 2
Problem:
@PostMapping
public ResponseEntity<CreateTaskResponse> create(
@RequestBody CreateTaskRequest request
) {
...
}
Validation test expects 400 for blank title, but controller accepts it.
Question:
What is missing?
Answer:
@Valid is missing.
Correct:
@PostMapping
public ResponseEntity<CreateTaskResponse> create(
@Valid @RequestBody CreateTaskRequest request
) {
...
}
61. Tiny Bug Practice 3
Problem:
mockMvc.perform(post("/api/tasks")
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
Question:
What is likely missing?
Answer:
The request content type is missing.
Correct:
.contentType(MediaType.APPLICATION_JSON)
62. Tiny Bug Practice 4
Problem:
@WebMvcTest(TaskController.class)
@AutoConfigureMockMvc(addFilters = false)
class TaskControllerSecurityTest {
}
Question:
What is the warning?
Answer:
Security filters are disabled. This test can check controller behavior, but it does not prove real security behavior.
Practice Questions and Answers
Question 1
What is a web layer test?
Answer:
A web layer test checks HTTP/controller behavior, such as request mapping, JSON, validation, response status, headers, and error responses.
Question 2
What does @WebMvcTest test?
Answer:
It tests the Spring MVC slice, especially controllers, controller advice, JSON conversion, validation, and MockMvc behavior.
Question 3
Does @WebMvcTest load the full application context?
Answer:
No. It loads only a focused MVC test slice.
Question 4
Are services usually real or mocked in @WebMvcTest?
Answer:
They are usually mocked with @MockitoBean or, in older projects, @MockBean.
Question 5
What is MockMvc?
Answer:
MockMvc is a tool for performing HTTP-like requests through Spring MVC infrastructure without starting a real server.
Question 6
How do I send query parameters with MockMvc?
Answer:
Use .param("name", "value").
Question 7
How do I send JSON with MockMvc?
Answer:
Use .contentType(MediaType.APPLICATION_JSON) and .content(...).
Question 8
Why use ObjectMapper in web tests?
Answer:
It creates JSON from Java objects using Jackson configuration and avoids manual JSON strings.
Question 9
What annotation triggers validation for @RequestBody?
Answer:
@Valid.
Question 10
What status should invalid validation usually return?
Answer:
400 Bad Request.
Question 11
How do I verify that service was not called?
Answer:
Use verify(service, never()).method(any()).
Question 12
How do I test JSON response fields?
Answer:
Use jsonPath, for example jsonPath("$.title").value("Task A").
Question 13
How do I test controller advice?
Answer:
Mock the service to throw an exception, perform the request, and assert the status and JSON error body returned by @RestControllerAdvice.
Question 14
What status is commonly returned for malformed JSON?
Answer:
400 Bad Request.
Question 15
What status can wrong content type return?
Answer:
415 Unsupported Media Type.
Question 16
How do I test a custom header?
Answer:
Use .header("Header-Name", "value").
Question 17
How do I test security with @WebMvcTest?
Answer:
Use Spring Security test tools like @WithMockUser, .with(user(...)), .with(jwt()), and .with(csrf()).
Question 18
When do I need .with(csrf())?
Answer:
For unsafe HTTP methods such as POST, PUT, PATCH, DELETE when CSRF protection is enabled.
Question 19
What does @AutoConfigureMockMvc(addFilters = false) do?
Answer:
It disables filters, including security filters, for MockMvc tests.
Question 20
When should I use @SpringBootTest instead of @WebMvcTest?
Answer:
Use @SpringBootTest when I need the full application context or want to test multiple layers together, such as controller, service, repository, security, and database.
Final Memory Sentences
- Web layer tests prove HTTP behavior.
@WebMvcTestis a Spring MVC slice test.@WebMvcTestdoes not load the full app.- Services are usually mocked in
@WebMvcTest. - MockMvc performs requests without a real server.
- Use
.param(...)for query parameters. - Use
.contentType(APPLICATION_JSON)for JSON requests. - Use ObjectMapper for complex JSON.
- Validation needs
@Valid. - Validation errors usually return 400.
- Invalid JSON usually returns 400.
- Wrong content type can return 415.
- Use
jsonPathto test JSON fields. - Use controller advice tests to protect API error format.
- Security may be active in
@WebMvcTest. - Use
@WithMockUserfor authenticated users. - Use
.with(csrf())when CSRF is enabled for unsafe methods. @AutoConfigureMockMvc(addFilters = false)disables security filters.@WebMvcTestis focused;@SpringBootTestis complete.- Test your API contract, not the framework itself.