Woche 7, Tag 3 — Web-Layer-Testing mit @WebMvcTest und MockMvc
Ziel
Heute verstehst du, wie du den Web-/Controller-Layer in Spring Boot testest.
Die Kernfragen:
- Was ist ein Web-Layer-Test?
- Was lädt
@WebMvcTest? - Was lädt
@WebMvcTestnicht? - Was ist MockMvc?
- Wie testest du GET-Endpoints?
- Wie testest du POST-Endpoints mit JSON?
- Wie testest du Request-Validation?
- Wie testest du JSON-Response-Felder?
- Wie testest du Exception Handling?
- Wie testest du Controller-Security?
- Was ist der Unterschied zwischen
@WebMvcTestund@SpringBootTest? - Was sind typische Prüfungsfallen?
1. Kurz-Wiederholung aus Woche 7, Tag 2
An Tag 2 hast du gelernt:
- Unit-Tests starten Spring nicht.
- JUnit führt Tests aus.
- AssertJ prüft Ergebnisse flüssig.
- Mockito mockt Abhängigkeiten.
- Stub bedeutet gefälschte Rückgabe.
- Verify bedeutet Interaktion prüfen.
ArgumentCaptorfängt an Mocks übergebene Argumente ab.- Unit-Tests eignen sich für Service-Business-Logik.
- Unit-Tests beweisen kein Spring MVC, keine JSON-Konvertierung, keine Validation und keine Security-Filter.
Merksatz:
Wenn der Test Spring nicht braucht, starte Spring nicht.
Heute testest du den Teil, der Spring MVC braucht:
Controller + Request Mapping + JSON + Validation + HTTP-Response.
2. Was ist ein Web-Layer-Test?
Ein Web-Layer-Test prüft den Controller-Layer.
Er beantwortet Fragen wie:
Ruft diese URL die richtige Controller-Methode auf?
Liest der Controller Path Variables korrekt?
Liest er Query-Parameter korrekt?
Deserialisiert er den JSON-Request-Body korrekt?
Liefert Validation bei ungültiger Eingabe 400?
Hat die Response den richtigen HTTP-Status?
Enthält die Response die richtigen JSON-Felder?
Liefert @ControllerAdvice die richtige Fehlerantwort?
Blockiert oder erlaubt Spring Security diese Anfrage?
Merksatz:
Web-Layer-Tests beweisen HTTP-Verhalten.
3. Was ist @WebMvcTest?
@WebMvcTest lädt einen fokussierten Spring-MVC-Test-Slice.
Er enthält üblicherweise:
Controller
Controller Advice
JSON-Konvertierung
Validation-Unterstützung
MockMvc
Spring-MVC-Infrastruktur
Security-Filter-Unterstützung standardmäßig
Er lädt üblicherweise nicht:
normale Service-Beans
Repositories
Datenbank
voller Application Context
externe Clients
Scheduled Jobs
gesamte Auto-Configuration
Merksatz:
@WebMvcTesttestet den MVC-Slice, nicht die ganze App.
4. Warum @WebMvcTest nutzen?
Im Vergleich zu @SpringBootTest ist @WebMvcTest:
schneller
fokussierter
besser für Controller-Verhalten
einfacher, HTTP-Verhalten zu isolieren
Beispiel:
@WebMvcTest(TaskController.class)
class TaskControllerTest {
}
Das bedeutet:
Lade MVC-Unterstützung nur für TaskController.
Merksatz:
Nutze
@WebMvcTest, wenn du Controller-Verhalten ohne die volle App testen willst.
5. Was ist MockMvc?
Mit MockMvc führst du HTTP-ähnliche Anfragen aus, ohne einen echten Server zu starten.
Beispiel:
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk());
MockMvc läuft durch die Spring-MVC-Infrastruktur.
Damit kannst du testen:
Request Mapping
Path Variables
Query-Parameter
Request Body
JSON-Konvertierung
Validation
Controller Advice
Response-Status
Response Body
Header
Security-Filter
Merksatz:
MockMvc testet MVC-Anfragen ohne echten HTTP-Server.
6. Grundstruktur von @WebMvcTest
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"));
}
}
Wichtig:
Controller ist echt.
TaskService ist gemockt.
HTTP-Verhalten wird getestet.
Merksatz:
In
@WebMvcTestmockst du die Controller-Kollaborateure.
7. @MockitoBean vs @MockBean
In modernen Spring-Tests siehst du vielleicht:
@MockitoBean
private TaskService taskService;
In vielen älteren Spring-Boot-Projekten siehst du vielleicht:
@MockBean
private TaskService taskService;
Sie dienen hier dem gleichen Lernzweck:
ersetze eine Spring-Bean durch einen Mockito-Mock im Test-Context
In diesem Buch nutzen wir:
@MockitoBean
Wenn dein Projekt eine ältere Spring-Boot-Version nutzt, verwende:
@MockBean
Merksatz:
Nutze eine Mockito-Spring-Bean, wenn der Controller eine Service-Abhängigkeit braucht.
8. Beispiel-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
) {
}
Merksatz:
Web-Tests arbeiten üblicherweise mit Request- und Response-DTOs.
9. Controller mit GET und 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);
}
}
Dieser Controller behandelt:
GET /api/tasks/{id}
POST /api/tasks
10. GET mit Path Variable testen
@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);
}
Das testet:
Path Variable wird korrekt gelesen
Service wird mit id 1 aufgerufen
Response-Status ist 200
Response-JSON ist korrekt
Merksatz:
Nutze MockMvc-GET-Tests, um Path Variables und JSON-Response zu prüfen.
11. Query-Parameter testen
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);
}
Merksatz:
Nutze
.param(...)für Query-Parameter.
12. Standard-Query-Parameter testen
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);
}
Das beweist:
page ist standardmäßig 0
size ist standardmäßig 20
Merksatz:
Web-Tests können Standardwerte von Request-Parametern prüfen.
13. POST mit JSON testen
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"));
}
Das testet:
JSON-Request-Body wird akzeptiert
Controller liefert 201 Created
Location-Header ist gesetzt
JSON-Response ist korrekt
Merksatz:
Nutze
.contentType(APPLICATION_JSON)und.content(...)für JSON-Request-Bodies.
14. ObjectMapper in Tests nutzen
Statt JSON manuell zu schreiben, kannst du ObjectMapper nutzen.
@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"));
}
Vorteile:
weniger manuelles JSON-Tippen
nutzt dieselbe Jackson-Konfiguration
sicherer, wenn sich das DTO ändert
Merksatz:
Nutze ObjectMapper, wenn JSON komplex wird.
15. Request-Body testen, der an den Service weitergegeben wird
Manchmal willst du prüfen, welches Request-Objekt den Service erreicht hat.
Nutze 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");
}
Merksatz:
Nutze ArgumentCaptor, wenn du Daten prüfen musst, die vom Controller an den Service weitergegeben werden.
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);
}
Wichtig:
@Valid löst Validation am Request-Body aus.
Ohne @Valid laufen Validierungs-Annotations am DTO möglicherweise nicht.
Merksatz:
Validation braucht
@Validam Controller-Parameter.
17. Validierungsfehler testen
Leeren Titel testen:
@Test
void returnsBadRequestWhenTitleIsBlank() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": ""
}
"""))
.andExpect(status().isBadRequest());
verify(taskService, never()).create(any());
}
Das beweist:
ungültige Anfrage liefert 400
Service wird nicht aufgerufen
Merksatz:
Ungültige Anfrage sollte 400 liefern, bevor die Service-Logik läuft.
18. Validierungsfehler-Body testen
Für eine saubere Fehlerantwort erstellst du 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"));
}
Merksatz:
Web-Tests sollten das Fehlerformat prüfen, auf das Clients angewiesen sind.
19. Fehlenden Request-Body testen
Controller erwartet:
@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());
}
Merksatz:
Fehlender JSON-Body sollte üblicherweise 400 liefern.
20. Ungültiges JSON testen
Fehlerhaftes JSON testen:
@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());
}
Das testet:
Jackson kann Request-Body nicht parsen
Controller-Methode wird nicht erfolgreich aufgerufen
Service wird nicht aufgerufen
Merksatz:
Ungültiges JSON sollte vor der Service-Logik fehlschlagen.
21. Falschen Content-Type testen
Wenn der Endpoint JSON konsumiert, kann ein falscher Content-Type fehlschlagen.
Test:
@Test
void returnsUnsupportedMediaTypeForTextPlain() throws Exception {
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.TEXT_PLAIN)
.content("title=Task A"))
.andExpect(status().isUnsupportedMediaType());
}
Erwartet:
415 Unsupported Media Type
abhängig von Controller-Konfiguration und Converters.
Merksatz:
Web-Tests können Content-Type-Verhalten prüfen.
22. Response-Header testen
Controller:
URI location = URI.create("/api/tasks/" + response.id());
return ResponseEntity
.created(location)
.body(response);
Test:
.andExpect(header().string("Location", "/api/tasks/1"))
Vollständiges Beispiel:
@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"));
}
Merksatz:
Test Header when clients rely on them.
23. No-Content-Response testen
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);
}
Merksatz:
204 No Content should not return a Response Body.
24. Exception Handling testen
Service wirft:
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"));
}
Merksatz:
Web tests should verify Controller Advice error responses.
25. Business-Exceptions testen
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"));
}
Merksatz:
Controller-Advice-Tests schützen den API-Fehlervertrag.
26. Arrays testen
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"));
}
Merksatz:
Nutze
$[0],$[1]für JSON-Arrays.
27. Leere Arrays testen
@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());
}
Merksatz:
Leere Listen-Responses sollten ebenfalls getestet werden.
28. Datum/Zeit-JSON testen
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"));
}
Merksatz:
Teste Datum/Zeit-JSON, wenn API-Clients vom Format abhängen.
29. Enum-JSON testen
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"));
}
Merksatz:
Web-Tests können das Enum-JSON-Format schützen.
30. Security mit @WebMvcTest testen
@WebMvcTest enthält oft Spring Security.
Wenn der Endpoint Authentifizierung braucht, kann dieser Test 401 liefern:
@Test
void anonymousCannotAccessTasks() throws Exception {
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isUnauthorized());
}
Für authentifizierten 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());
}
Merksatz:
Web-Layer-Tests können Security-Verhalten einschließen.
31. Rollenbasierten Zugriff testen
Security-Regel:
.requestMatchers("/api/admin/**").hasRole("ADMIN")
Verweigerung testen:
@Test
@WithMockUser(roles = "USER")
void userCannotAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
Erlaubnis testen:
@Test
@WithMockUser(roles = "ADMIN")
void adminCanAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
Merksatz:
Teste sowohl erlaubte als auch verweigerte Security-Fälle.
32. CSRF in Web-Layer-Tests testen
Wenn CSRF aktiv ist, brauchen unsichere Methoden ein CSRF-Token.
Unsichere Methoden:
POST
PUT
PATCH
DELETE
Mit CSRF testen:
@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());
}
Fehlendes CSRF testen:
@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());
}
Merksatz:
permitAllund die richtige Authority umgehen CSRF nicht automatisch.
33. Security-Filter in Controller-Tests deaktivieren
Manchmal willst du nur Controller-Verhalten testen, nicht Security.
Mögliche Option:
@WebMvcTest(TaskController.class)
@AutoConfigureMockMvc(addFilters = false)
class TaskControllerTest {
}
Das deaktiviert Filter, einschließlich Security-Filter.
Vorsichtig nutzen.
Gut für:
reine Controller-Mapping-Tests
wenn Security separat getestet wird
Risiko:
Test kann bestehen, obwohl echte Anfrage von Security blockiert wird
Merksatz:
Filter deaktivieren macht Controller-Tests einfacher, aber weniger realistisch.
34. Security-Config importieren
Wenn du eine bestimmte Security-Konfiguration in @WebMvcTest brauchst, kannst du sie importieren.
@WebMvcTest(TaskController.class)
@Import(SecurityConfig.class)
class TaskControllerSecurityTest {
}
Das ist nützlich, wenn:
Controller-Test soll deine Anwendungs-Security-Regeln nutzen
Security-Beans werden nicht automatisch geladen
Custom Security Config ist nötig
Merksatz:
Importiere Security Config, wenn der Web-Slice deine echten Security-Regeln braucht.
35. Custom Principal testen
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"));
}
Merksatz:
Nutze
.with(user(customPrincipal)), wenn der Controller einen Custom Principal erwartet.
36. JWT im Web-Layer testen
Wenn Controller/Security einen JWT Resource Server nutzt, kannst du JWT mocken.
@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());
}
Verweigerung testen:
@Test
void jwtWithoutReadScopeIsForbidden() throws Exception {
mockMvc.perform(get("/api/tasks/1")
.with(jwt().authorities(
new SimpleGrantedAuthority("SCOPE_task:write")
)))
.andExpect(status().isForbidden());
}
Merksatz:
Nutze
jwt()für Web-Tests der Resource-Server-Autorisierung.
37. Multipart-Upload testen
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"));
}
Merksatz:
MockMvc kann auch Multipart-Anfragen testen.
38. Header testen
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);
}
Merksatz:
Nutze
.header(...), um Request-Header zu testen.
39. Fehlenden Header testen
@Test
void returnsBadRequestWhenTenantHeaderMissing() throws Exception {
mockMvc.perform(get("/api/tasks"))
.andExpect(status().isBadRequest());
verify(taskService, never()).listForTenant(any());
}
Merksatz:
Missing required Header usually return 400.
40. Cookies testen
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"));
}
Merksatz:
MockMvc kann Cookies mit
.cookie(...)senden.
41. @WebMvcTest vs @SpringBootTest
| Thema | @WebMvcTest | @SpringBootTest |
|---|---|---|
| Umfang | MVC-Slice | voller Application Context |
| Geschwindigkeit | schneller | langsamer |
| Service | meist gemockt | echt, sofern nicht gemockt |
| Repository | nicht geladen | geladen, wenn im Context |
| Datenbank | nein | möglich |
| Am besten für | Controller-Verhalten | vollständiger Flow/Integration |
| MockMvc | automatisch konfiguriert | @AutoConfigureMockMvc nutzen |
Merksatz:
@WebMvcTestist fokussiert;@SpringBootTestist vollständig.
42. Wann @WebMvcTest nutzen
Nutze @WebMvcTest für:
Request Mapping
Path Variables
Query-Parameter
Request-Header
Request-Body-JSON
Validation
Response-Status
Response-JSON
Controller Advice
Security auf Controller-Ebene
Nutze es nicht, um zu beweisen:
Service-Business-Logik
Repository-Queries
Datenbank-Mapping
Transaktionsverhalten
vollständigen Anwendungsflow
Merksatz:
Nutze Web-Tests für den HTTP-Vertrag, nicht für Service-Logik.
43. Was du nicht übertesten solltest
Teste Spring selbst nicht.
Nicht nützlich:
Funktioniert @GetMapping überhaupt?
Serialisiert Jackson generell JSON?
Funktioniert MockMvc?
Nützlich:
Funktioniert meine Endpoint-URL?
Validiert mein Request-DTO korrekt?
Entspricht meine Fehlerantwort meinem API-Vertrag?
Ruft mein Controller den Service mit korrekten Daten auf?
Blockiert meine Security-Regel falsche User?
Merksatz:
Teste deinen API-Vertrag, nicht das Framework allgemein.
44. Häufige Fehler bei Web-Tests
Fehler 1:
Service-Abhängigkeit vergessen zu mocken.
Fehler 2:
Service-Business-Logik in @WebMvcTest testen.
Fehler 3:
@Valid im Controller vergessen und sich wundern, warum der Validation-Test fehlschlägt.
Fehler 4:
contentType(APPLICATION_JSON) vergessen.
Fehler 5:
201 erwarten, aber Controller liefert 200.
Fehler 6:
CSRF für POST/PUT/PATCH/DELETE vergessen, wenn CSRF aktiv ist.
Fehler 7:
Security-Filter deaktivieren und denken, Security sei getestet.
Fehler 8:
@SpringBootTest für jeden Controller-Test nutzen.
Fehler 9:
Jede Service-Interaktion übermäßig verifizieren.
Fehler 10:
Fehlerantworten nicht testen.
45. Typische Prüfungsfallen
Falle 1
@WebMvcTest ist ein Slice-Test.
Falle 2
@WebMvcTest lädt nicht den vollen Application Context.
Falle 3
Services sind in @WebMvcTest üblicherweise gemockt.
Falle 4
MockMvc führt Anfragen ohne echten Server aus.
Falle 5
@RequestBody-JSON wird über HTTP Message Converters deserialisiert.
Falle 6
Validation am Request-Body braucht @Valid.
Falle 7
Ungültige Validation liefert üblicherweise 400.
Falle 8
Fehlerhaftes JSON liefert üblicherweise 400.
Falle 9
Falscher Content-Type kann 415 liefern.
Falle 10
@RestControllerAdvice kann mit @WebMvcTest getestet werden.
Falle 11
Security-Filter können in @WebMvcTest aktiv sein.
Falle 12
POST-Tests brauchen möglicherweise CSRF, wenn CSRF aktiv ist.
Falle 13
@AutoConfigureMockMvc(addFilters = false) deaktiviert Security-Filter.
Falle 14
Filter deaktivieren bedeutet, Security wird nicht getestet.
Falle 15
Nutze @SpringBootTest, wenn du echten Service/Repository/vollständigen Flow brauchst.
46. Echte Prüfungsfrage: @WebMvcTest
Frage:
Wofür wird @WebMvcTest genutzt?
Antwort:
@WebMvcTest testet den Spring-MVC-Web-Layer — besonders Controller, Request Mapping, Validation, JSON-Serialisierung/Deserialisierung, Controller Advice und MockMvc-Verhalten.
47. Echte Prüfungsfrage: MockMvc
Frage:
Was ist MockMvc?
Antwort:
MockMvc ist ein Spring-MVC-Test-Tool, das HTTP-ähnliche Anfragen über die Spring-MVC-Infrastruktur ausführt, ohne einen echten Webserver zu starten.
48. Echte Prüfungsfrage: Service in @WebMvcTest
Frage:
Werden Service-Beans in @WebMvcTest normalerweise geladen?
Antwort:
Nein. @WebMvcTest fokussiert den Web-Layer — Service-Abhängigkeiten werden üblicherweise mit @MockitoBean oder ähnlichen Test-Mock-Annotations gemockt.
49. Echte Prüfungsfrage: Validation
Frage:
Welche Annotation löst Validation am Request-Body aus?
Antwort:
@Valid am @RequestBody-Controller-Parameter.
50. Echte Prüfungsfrage: Ungültige Anfrage
Frage:
Welcher HTTP-Status wird üblicherweise bei Validierungsfehlern zurückgegeben?
Antwort:
400 Bad Request.
51. Echte Prüfungsfrage: Controller Advice
Frage:
Kann @RestControllerAdvice in Web-Layer-Tests getestet werden?
Antwort:
Ja. @WebMvcTest kann Controller Advice testen und Fehlerantworten prüfen.
52. Echte Prüfungsfrage: CSRF in Web-Tests
Frage:
Was braucht eine POST-Anfrage in einem MockMvc-Test, wenn CSRF aktiv ist?
Antwort:
Sie braucht .with(csrf()).
53. Interview-Antwort
Frage:
Wann würdest du
@WebMvcTestnutzen?
Gute Antwort:
Ich nutze @WebMvcTest, wenn ich den Controller-Layer testen will, ohne die volle Anwendung zu starten. Es eignet sich für Request Mapping, Path Variables, Query-Parameter, Request-Body-JSON, Validation, Response-Status, Response-JSON und Controller-Exception-Handling. Service-Abhängigkeiten mocke ich, weil der Test HTTP-Verhalten prüfen soll — nicht Service- oder Datenbank-Logik.
54. Interview-Antwort
Frage:
Was ist der Unterschied zwischen
@WebMvcTestund@SpringBootTest?
Gute Antwort:
@WebMvcTest lädt nur den MVC-Slice — deshalb schneller und auf Controller fokussiert. Services und Repositories werden üblicherweise nicht geladen und müssen gemockt werden. @SpringBootTest lädt den vollen Application Context und nutzt du, wenn mehrere Layer zusammenarbeiten sollen — Controller, Service, Repository, Security und Datenbank-Integration.
55. Interview-Antwort
Frage:
Wie testest du Validation in einem Controller?
Gute Antwort:
Ich erstelle ein Request-DTO mit Bean-Validation-Annotations, annotiere den Controller-Parameter mit @Valid und sende mit MockMvc ungültiges JSON. Ich erwarte 400 Bad Request und prüfe bei Custom-Error-Format die JSON-Fehlerantwort. Außerdem verifiziere ich, dass der Service nicht aufgerufen wurde.
56. Interview-Antwort
Frage:
Wie testest du Controller-Exception-Handling?
Gute Antwort:
Ich mocke den Service so, dass er eine bestimmte Exception wirft — z. B. ResourceNotFoundException. Dann führe ich die Anfrage mit MockMvc aus und prüfe HTTP-Status und JSON-Fehlerbody von @RestControllerAdvice.
57. Interview-Antwort
Frage:
Wie wirken Security-Filter auf
@WebMvcTest?
Gute Antwort:
@WebMvcTest kann Spring-Security-Filter einschließen — geschützte Endpoints liefern in Tests möglicherweise 401 oder 403. Du kannst @WithMockUser, user(), jwt() oder csrf() aus Spring-Security-Test-Support nutzen. Deaktivierst du Filter mit @AutoConfigureMockMvc(addFilters = false), testest du kein Security-Verhalten mehr.
58. Kleine Code-Übung
Controller:
@PostMapping("/api/tasks")
public ResponseEntity<CreateTaskResponse> create(
@Valid @RequestBody CreateTaskRequest request
) {
CreateTaskResponse response = taskService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
Schreibe Tests für:
gültige Anfrage -> 201
leerer Titel -> 400
ungültiges JSON -> 400
Mögliche Antwort:
@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. Kleine Bug-Übung 1
Problem:
@WebMvcTest(TaskController.class)
class TaskControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void returnsTask() throws Exception {
mockMvc.perform(get("/api/tasks/1"))
.andExpect(status().isOk());
}
}
Der Test schlägt fehl, weil die TaskService-Bean fehlt.
Frage:
Was fehlt?
Antwort:
Die Service-Abhängigkeit sollte gemockt werden.
@MockitoBean
private TaskService taskService;
Ältere Projekte nutzen vielleicht:
@MockBean
private TaskService taskService;
60. Kleine Bug-Übung 2
Problem:
@PostMapping
public ResponseEntity<CreateTaskResponse> create(
@RequestBody CreateTaskRequest request
) {
...
}
Validation-Test erwartet 400 bei leerem Titel, aber Controller akzeptiert ihn.
Frage:
Was fehlt?
Antwort:
@Valid fehlt.
Korrekt:
@PostMapping
public ResponseEntity<CreateTaskResponse> create(
@Valid @RequestBody CreateTaskRequest request
) {
...
}
61. Kleine Bug-Übung 3
Problem:
mockMvc.perform(post("/api/tasks")
.content("""
{"title":"Task A"}
"""))
.andExpect(status().isCreated());
Frage:
Was fehlt wahrscheinlich?
Antwort:
Der Request-Content-Type fehlt.
Korrekt:
.contentType(MediaType.APPLICATION_JSON)
62. Kleine Bug-Übung 4
Problem:
@WebMvcTest(TaskController.class)
@AutoConfigureMockMvc(addFilters = false)
class TaskControllerSecurityTest {
}
Frage:
Was ist die Warnung?
Antwort:
Security-Filter sind deaktiviert. Der Test kann Controller-Verhalten prüfen, beweist aber kein echtes Security-Verhalten.
Übungsfragen
Frage 1
Was ist ein Web-Layer-Test?
Antwort:
Ein Web-Layer-Test prüft HTTP-/Controller-Verhalten — Request Mapping, JSON, Validation, Response-Status, Header und Fehlerantworten.
Frage 2
Was testet @WebMvcTest?
Antwort:
Er testet den Spring-MVC-Slice — besonders Controller, Controller Advice, JSON-Konvertierung, Validation und MockMvc-Verhalten.
Frage 3
Lädt @WebMvcTest den vollen Application Context?
Antwort:
Nein. Er lädt nur einen fokussierten MVC-Test-Slice.
Frage 4
Sind Services in @WebMvcTest üblicherweise echt oder gemockt?
Antwort:
Sie sind üblicherweise mit @MockitoBean oder in älteren Projekten mit @MockBean gemockt.
Frage 5
Was ist MockMvc?
Antwort:
MockMvc führt HTTP-ähnliche Anfragen über die Spring-MVC-Infrastruktur aus, ohne einen echten Server zu starten.
Frage 6
Wie sendest du Query-Parameter mit MockMvc?
Antwort:
Nutze .param("name", "value").
Frage 7
Wie sendest du JSON mit MockMvc?
Antwort:
Nutze .contentType(MediaType.APPLICATION_JSON) und .content(...).
Frage 8
Warum ObjectMapper in Web-Tests nutzen?
Antwort:
Er erstellt JSON aus Java-Objekten mit Jackson-Konfiguration und vermeidet manuelle JSON-Strings.
Frage 9
Welche Annotation löst Validation für @RequestBody aus?
Antwort:
@Valid.
Frage 10
Welchen Status sollte ungültige Validation üblicherweise liefern?
Antwort:
400 Bad Request.
Frage 11
Wie verifizierst du, dass der Service nicht aufgerufen wurde?
Antwort:
Nutze verify(service, never()).method(any()).
Frage 12
Wie testest du JSON-Response-Felder?
Antwort:
Nutze jsonPath, z. B. jsonPath("$.title").value("Task A").
Frage 13
Wie testest du Controller Advice?
Antwort:
Mocke den Service so, dass er eine Exception wirft, führe die Anfrage aus und prüfe Status und JSON-Fehlerbody von @RestControllerAdvice.
Frage 14
Welcher Status wird üblicherweise bei fehlerhaftem JSON zurückgegeben?
Antwort:
400 Bad Request.
Frage 15
Welchen Status kann ein falscher Content-Type liefern?
Antwort:
415 Unsupported Media Type.
Frage 16
Wie testest du einen Custom Header?
Antwort:
Nutze .header("Header-Name", "value").
Frage 17
Wie testest du Security mit @WebMvcTest?
Antwort:
Nutze Spring-Security-Test-Tools wie @WithMockUser, .with(user(...)), .with(jwt()) und .with(csrf()).
Frage 18
Wann brauchst du .with(csrf())?
Antwort:
Für unsichere HTTP-Methoden wie POST, PUT, PATCH, DELETE, wenn CSRF-Schutz aktiv ist.
Frage 19
Was macht @AutoConfigureMockMvc(addFilters = false)?
Antwort:
Es deaktiviert Filter, einschließlich Security-Filter, für MockMvc-Tests.
Frage 20
Wann solltest du @SpringBootTest statt @WebMvcTest nutzen?
Antwort:
Nutze @SpringBootTest, wenn du den vollen Application Context brauchst oder mehrere Layer zusammen testen willst — Controller, Service, Repository, Security und Datenbank.
Finale Merksätze
- Web-Layer-Tests beweisen HTTP-Verhalten.
@WebMvcTestist ein Spring-MVC-Slice-Test.@WebMvcTestlädt nicht die volle App.- Services sind in
@WebMvcTestüblicherweise gemockt. - MockMvc führt Anfragen ohne echten Server aus.
- Nutze
.param(...)für Query-Parameter. - Nutze
.contentType(APPLICATION_JSON)für JSON-Anfragen. - Nutze ObjectMapper für komplexes JSON.
- Validation braucht
@Valid. - Validierungsfehler liefern üblicherweise 400.
- Ungültiges JSON liefert üblicherweise 400.
- Falscher Content-Type kann 415 liefern.
- Nutze
jsonPath, um JSON-Felder zu testen. - Nutze Controller-Advice-Tests, um das API-Fehlerformat zu schützen.
- Security kann in
@WebMvcTestaktiv sein. - Nutze
@WithMockUserfür authentifizierte User. - Nutze
.with(csrf()), wenn CSRF für unsichere Methoden aktiv ist. @AutoConfigureMockMvc(addFilters = false)deaktiviert Security-Filter.@WebMvcTestist fokussiert;@SpringBootTestist vollständig.- Teste deinen API-Vertrag, nicht das Framework selbst.