Zum Hauptinhalt springen

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:

  1. Was ist ein Web-Layer-Test?
  2. Was lädt @WebMvcTest?
  3. Was lädt @WebMvcTest nicht?
  4. Was ist MockMvc?
  5. Wie testest du GET-Endpoints?
  6. Wie testest du POST-Endpoints mit JSON?
  7. Wie testest du Request-Validation?
  8. Wie testest du JSON-Response-Felder?
  9. Wie testest du Exception Handling?
  10. Wie testest du Controller-Security?
  11. Was ist der Unterschied zwischen @WebMvcTest und @SpringBootTest?
  12. 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.
  • ArgumentCaptor fä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:

@WebMvcTest testet 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 @WebMvcTest mockst 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 @Valid am 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:

permitAll und 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
UmfangMVC-Slicevoller Application Context
Geschwindigkeitschnellerlangsamer
Servicemeist gemocktecht, sofern nicht gemockt
Repositorynicht geladengeladen, wenn im Context
Datenbankneinmöglich
Am besten fürController-Verhaltenvollständiger Flow/Integration
MockMvcautomatisch konfiguriert@AutoConfigureMockMvc nutzen

Merksatz:

@WebMvcTest ist fokussiert; @SpringBootTest ist 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 @WebMvcTest nutzen?

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 @WebMvcTest und @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.
  • @WebMvcTest ist ein Spring-MVC-Slice-Test.
  • @WebMvcTest lä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 @WebMvcTest aktiv sein.
  • Nutze @WithMockUser für authentifizierte User.
  • Nutze .with(csrf()), wenn CSRF für unsichere Methoden aktiv ist.
  • @AutoConfigureMockMvc(addFilters = false) deaktiviert Security-Filter.
  • @WebMvcTest ist fokussiert; @SpringBootTest ist vollständig.
  • Teste deinen API-Vertrag, nicht das Framework selbst.