Spring Boot Aufbau – Tag 8 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting

📍 Deine Position im Kurs
| Tag | Thema | Status |
|---|---|---|
| 1 | Auto-Configuration & Custom Starter | Abgeschlossen |
| 2 | Spring Data JPA Basics | Abgeschlossen |
| 3 | JPA Relationships & Queries | Abgeschlossen |
| 4 | Spring Security Part 1 – Authentication | Abgeschlossen |
| 5 | Spring Security Part 2 – Authorization | Abgeschlossen |
| 6 | Spring Boot Caching & JSON | Abgeschlossen |
| 7 | Messaging & Email | Abgeschlossen |
| → 8 | Testing & Dokumentation | 👉 DU BIST HIER! |
| 9 | Spring Boot Actuator | Noch nicht freigeschaltet |
| 10 | Template Engines & Microservices | Noch nicht freigeschaltet |
Modul: Spring Boot Aufbau (10 Arbeitstage)
Dauer heute: 8 Stunden
Dein Ziel: Testbare Anwendungen schreiben und APIs dokumentieren
📋 Voraussetzungen
Du brauchst:
- ✅ Spring Boot Basics (Tag 1-7)
- ✅ REST Controller Kenntnisse (Tag 2)
- ✅ Service-Layer Pattern verstanden (Tag 2)
Optional (hilft beim Verständnis):
- JUnit Grundkenntnisse
- HTTP-Basics
Tag verpasst? Kein Problem! Dieser Blogbeitrag deckt genau den Stoff von Tag 8 ab. Download das Projekt und arbeite die 8 Stunden durch!
⚡ Was du heute baust
Ein vollständig getestetes User-Management-System mit Unit Tests, Integration Tests und automatisch generierter API-Dokumentation. Du lernst, wie man Code testet, der von Anfang an testbar designed wurde!
🎯 Dein Ziel nach 8 Stunden
- ✅ Unit Tests mit JUnit 5 & Mockito geschrieben
- ✅ Integration Tests mit @SpringBootTest erstellt
- ✅ Test Coverage über 80%
- ✅ OpenAPI/Swagger Dokumentation generiert
- ✅ TDD-Workflow verstanden und angewendet
💻 Los geht’s!
Hi, Java-Entwickler! 👋
Elyndra hier – und ich muss dir von meinem schlimmsten Production-Bug erzählen. Vor 3 Jahren hab ich einen Payment-Service ohne Tests deployed. „Ist ja nur ein Refactoring“, dachte ich.
Was passierte:
// Vorher (funktionierte)
public void transferMoney(Long from, Long to, BigDecimal amount) {
Account source = accountRepository.findById(from).get();
Account target = accountRepository.findById(to).get();
source.setBalance(source.getBalance().subtract(amount));
target.setBalance(target.getBalance().add(amount));
accountRepository.save(source);
accountRepository.save(target);
}
// Nachher ("Refactored" - ABER BROKEN!)
public void transferMoney(Long from, Long to, BigDecimal amount) {
Account source = accountRepository.findById(from).get();
Account target = accountRepository.findById(to).get();
// ❌ BUG: Reihenfolge vertauscht!
target.setBalance(target.getBalance().add(amount));
source.setBalance(source.getBalance().subtract(amount));
accountRepository.save(source);
accountRepository.save(target);
}
Das Ergebnis:
- ❌ Account-Balance wurde doppelt addiert statt subtrahiert
- ❌ Users bekamen Geld aus dem Nichts
- ❌ 20.000€ Schaden in 2 Stunden
- ❌ Weekend-Hotfix-Marathon
Die Lektion: Tests hätten diesen Bug sofort gefunden. Seit diesem Tag schreibe ich IMMER Tests. Keine Ausnahmen.
Während ich den Legacy-Code durchging, erinnerte mich das an Marcus und seine alten Balken – manche Dinge im Leben sind schwerer zu refactoren als Code…
Heute zeige ich dir, wie du von Anfang an testbar entwickelst!
🟢 GRUNDLAGEN
📚 Theorie: Warum Testing?
Die Test-Pyramid:
/\
/ \ E2E Tests (wenige, teuer)
/────\
/ IT \ Integration Tests (einige, medium)
/────────\
/ Unit \ Unit Tests (viele, billig)
/────────────\
Meine Empfehlung:
- 70% Unit Tests – schnell, viele
- 20% Integration Tests – realistisch
- 10% E2E Tests – kritische User-Flows
Was ich IMMER teste:
- ✅ Business-Logic (Unit Tests)
- ✅ Service-Layer (Unit + Integration)
- ✅ Critical User-Flows (E2E)
- ✅ Error-Cases (alle Layers!)
Was ich NICHT teste:
- ❌ Getter/Setter (Lombok generiert die)
- ❌ Simple Constructors
- ❌ Framework-Code (Spring testet sein eigenes Zeug)
Schritt 1: Test Dependencies Setup
pom.xml – Dependencies hinzufügen:
<dependencies>
<!-- Spring Boot Test Starter (enthält JUnit 5, Mockito, AssertJ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- H2 Database für Tests -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Was bekommst du hier?
Mit spring-boot-starter-test bekommst du ein komplettes Test-Framework in einer Dependency:
- JUnit 5 – Das Test-Framework selbst
- Mockito – Für Mock-Objects
- AssertJ – Für bessere Assertions
- H2 Database – In-Memory Test-Database
Du musst nichts installieren, nichts konfigurieren – Spring Boot bringt ALLES mit!
Schritt 2: Projekt-Struktur für Tests
Wie organisierst du Tests?
src/
├── main/
│ └── java/
│ └── com/example/
│ ├── model/
│ │ └── User.java
│ ├── repository/
│ │ └── UserRepository.java
│ ├── service/
│ │ └── UserService.java
│ └── controller/
│ └── UserController.java
└── test/
└── java/
└── com/example/
├── model/
│ └── UserTest.java
├── repository/
│ └── UserRepositoryTest.java
├── service/
│ └── UserServiceTest.java
└── controller/
└── UserControllerTest.java
Das Prinzip:
Deine Test-Struktur spiegelt deine Code-Struktur 1:1. Für jede Klasse in src/main/java gibt es eine Test-Klasse in src/test/java mit dem gleichen Namen + „Test“.
Schritt 3: User Entity – Unser Test-Subject
User.java:
package com.example.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
private boolean active = true;
private LocalDateTime createdAt = LocalDateTime.now();
// Business Logic - DAS testen wir!
public boolean isValidEmail() {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
}
public boolean isValidUsername() {
return username != null && username.length() >= 3 && username.length() <= 20;
}
public boolean canLogin() {
return active && isValidEmail() && isValidUsername();
}
}
Die Business-Logic im Detail:
isValidEmail()– Email-Validierung mit RegexisValidUsername()– Username muss zwischen 3 und 20 Zeichen seincanLogin()– User kann nur einloggen wenn: aktiv + Email valid + Username valid
Diese Methoden haben keine Abhängigkeiten! Perfekt für Unit Tests!
Schritt 4: Erste Unit Tests
UserTest.java:
package com.example.model;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
import static org.assertj.core.api.Assertions.*;
@DisplayName("User Entity Tests")
class UserTest {
private User user;
@BeforeEach
void setUp() {
user = new User();
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setPassword("password123");
user.setActive(true);
}
@Test
@DisplayName("Should validate correct email")
void shouldValidateCorrectEmail() {
// Given
user.setEmail("valid@example.com");
// When
boolean result = user.isValidEmail();
// Then
assertThat(result).isTrue();
}
@Test
@DisplayName("Should reject invalid email")
void shouldRejectInvalidEmail() {
// Given
user.setEmail("invalid-email");
// When
boolean result = user.isValidEmail();
// Then
assertThat(result).isFalse();
}
@Test
@DisplayName("Should reject too short username")
void shouldRejectTooShortUsername() {
// Given
user.setUsername("ab"); // Nur 2 Zeichen
// When
boolean result = user.isValidUsername();
// Then
assertThat(result).isFalse();
}
@Test
@DisplayName("Should allow login for valid active user")
void shouldAllowLoginForValidActiveUser() {
// When
boolean result = user.canLogin();
// Then
assertThat(result).isTrue();
}
@Test
@DisplayName("Should deny login for inactive user")
void shouldDenyLoginForInactiveUser() {
// Given
user.setActive(false);
// When
boolean result = user.canLogin();
// Then
assertThat(result).isFalse();
}
}
Das Given-When-Then Pattern:
- Given – Vorbereitung / Arrange
- When – Aktion / Act
- Then – Überprüfung / Assert
@BeforeEach wird vor JEDEM Test ausgeführt. Jeder Test bekommt einen frischen User!
Tests ausführen:
# Alle Tests mvn test # Nur UserTest mvn test -Dtest=UserTest
🟡 PROFESSIONAL
📚 Theorie: Repository & Service Testing
@DataJpaTest – Für Repository-Tests:
- Startet In-Memory H2 Database
- Konfiguriert Spring Data JPA
- Lädt NUR Repository-Komponenten
- Führt Rollback nach jedem Test aus
Mockito – Für Service-Tests:
- Erstellt Fake-Objects (Mocks)
- Simuliert Dependency-Verhalten
- Isoliert Unit-Tests
- Verifiziert Methoden-Aufrufe
Schritt 5: Repository Tests mit @DataJpaTest
UserRepository.java:
package com.example.repository;
import com.example.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
List<User> findByActiveTrue();
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
UserRepositoryTest.java:
package com.example.repository;
import com.example.model.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
@DataJpaTest
@DisplayName("UserRepository Tests")
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository userRepository;
private User testUser;
@BeforeEach
void setUp() {
testUser = new User();
testUser.setUsername("testuser");
testUser.setEmail("test@example.com");
testUser.setPassword("password123");
testUser.setActive(true);
entityManager.persistAndFlush(testUser);
}
@Test
@DisplayName("Should find user by username")
void shouldFindUserByUsername() {
// When
Optional<User> found = userRepository.findByUsername("testuser");
// Then
assertThat(found).isPresent();
assertThat(found.get().getUsername()).isEqualTo("testuser");
}
@Test
@DisplayName("Should return empty when user not found")
void shouldReturnEmptyWhenUserNotFound() {
// When
Optional<User> found = userRepository.findByUsername("nonexistent");
// Then
assertThat(found).isEmpty();
}
@Test
@DisplayName("Should find only active users")
void shouldFindOnlyActiveUsers() {
// Given
User inactiveUser = new User();
inactiveUser.setUsername("inactive");
inactiveUser.setEmail("inactive@example.com");
inactiveUser.setPassword("password");
inactiveUser.setActive(false);
entityManager.persistAndFlush(inactiveUser);
// When
List<User> activeUsers = userRepository.findByActiveTrue();
// Then
assertThat(activeUsers).hasSize(1);
assertThat(activeUsers.get(0).getUsername()).isEqualTo("testuser");
}
}
Der TestEntityManager:
Mit persistAndFlush() speicherst du Entities sofort in die Test-Database. Das garantiert, dass deine Query-Methods auch wirklich die DB treffen!
Schritt 6: Service Tests mit Mockito
UserService.java:
package com.example.service;
import com.example.model.User;
import com.example.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public User createUser(User user) {
log.info("Creating user: {}", user.getUsername());
if (!user.isValidUsername()) {
throw new IllegalArgumentException("Invalid username");
}
if (!user.isValidEmail()) {
throw new IllegalArgumentException("Invalid email");
}
if (userRepository.existsByUsername(user.getUsername())) {
throw new IllegalStateException("Username already exists");
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new IllegalStateException("Email already exists");
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
public List<User> findActiveUsers() {
return userRepository.findByActiveTrue();
}
@Transactional
public void deactivateUser(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("User not found"));
user.setActive(false);
userRepository.save(user);
}
}
UserServiceTest.java:
package com.example.service;
import com.example.model.User;
import com.example.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
@ExtendWith(MockitoExtension.class)
@DisplayName("UserService Tests")
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserService userService;
private User testUser;
@BeforeEach
void setUp() {
testUser = new User();
testUser.setId(1L);
testUser.setUsername("testuser");
testUser.setEmail("test@example.com");
testUser.setPassword("password123");
testUser.setActive(true);
}
@Test
@DisplayName("Should create user successfully")
void shouldCreateUserSuccessfully() {
// Given
when(userRepository.existsByUsername("testuser")).thenReturn(false);
when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
when(passwordEncoder.encode("password123")).thenReturn("hashed_password");
when(userRepository.save(any(User.class))).thenReturn(testUser);
// When
User created = userService.createUser(testUser);
// Then
assertThat(created).isNotNull();
assertThat(created.getUsername()).isEqualTo("testuser");
verify(userRepository).existsByUsername("testuser");
verify(userRepository).existsByEmail("test@example.com");
verify(passwordEncoder).encode("password123");
verify(userRepository).save(any(User.class));
}
@Test
@DisplayName("Should throw exception when username exists")
void shouldThrowExceptionWhenUsernameExists() {
// Given
when(userRepository.existsByUsername("testuser")).thenReturn(true);
// When & Then
assertThatThrownBy(() -> userService.createUser(testUser))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Username already exists");
verify(userRepository).existsByUsername("testuser");
verify(userRepository, never()).save(any(User.class));
}
@Test
@DisplayName("Should deactivate user")
void shouldDeactivateUser() {
// Given
when(userRepository.findById(1L)).thenReturn(Optional.of(testUser));
when(userRepository.save(any(User.class))).thenReturn(testUser);
// When
userService.deactivateUser(1L);
// Then
assertThat(testUser.isActive()).isFalse();
verify(userRepository).findById(1L);
verify(userRepository).save(testUser);
}
}
Die Mockito-Annotations:
- @Mock – Erstellt Fake-Objects
- @InjectMocks – Erstellt echte Instanz und injected Mocks
- when().thenReturn() – Definiert Mock-Verhalten
- verify() – Prüft ob Methode aufgerufen wurde
Mocks sind „dumme“ Objects – sie geben nur zurück, was du ihnen sagst!
Schritt 7: Integration Tests mit @SpringBootTest
UserIntegrationTest.java:
package com.example.integration;
import com.example.model.User;
import com.example.repository.UserRepository;
import com.example.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@ActiveProfiles("test")
@Transactional
@DisplayName("User Integration Tests")
class UserIntegrationTest {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
@DisplayName("Should create user with all components working together")
void shouldCreateUserWithAllComponents() {
// Given
User user = new User();
user.setUsername("integrationtest");
user.setEmail("integration@example.com");
user.setPassword("password123");
user.setActive(true);
// When
User created = userService.createUser(user);
// Then
assertThat(created.getId()).isNotNull();
assertThat(created.getUsername()).isEqualTo("integrationtest");
// Verify in Database
Optional<User> found = userRepository.findById(created.getId());
assertThat(found).isPresent();
assertThat(found.get().getPassword()).isNotEqualTo("password123"); // Hashed!
}
@Test
@DisplayName("Should prevent duplicate username")
void shouldPreventDuplicateUsername() {
// Given
User user1 = new User();
user1.setUsername("duplicate");
user1.setEmail("user1@example.com");
user1.setPassword("password");
User user2 = new User();
user2.setUsername("duplicate");
user2.setEmail("user2@example.com");
user2.setPassword("password");
// When
userService.createUser(user1);
// Then
assertThatThrownBy(() -> userService.createUser(user2))
.isInstanceOf(IllegalStateException.class)
.hasMessage("Username already exists");
}
}
@SpringBootTest:
- Startet den kompletten Spring-Context
- Lädt alle Beans
- Testet das Zusammenspiel aller Komponenten
- Langsamer, aber realistisch!
Schritt 8: Controller Tests mit @WebMvcTest
UserController.java:
package com.example.controller;
import com.example.model.User;
import com.example.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
return userService.findByUsername(username)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/active")
public ResponseEntity<List<User>> getActiveUsers() {
return ResponseEntity.ok(userService.findActiveUsers());
}
}
UserControllerTest.java:
package com.example.controller;
import com.example.model.User;
import com.example.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;
@WebMvcTest(UserController.class)
@DisplayName("UserController Tests")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
@Test
@DisplayName("Should create user via POST")
void shouldCreateUser() throws Exception {
// Given
User user = new User();
user.setId(1L);
user.setUsername("testuser");
user.setEmail("test@example.com");
when(userService.createUser(any(User.class))).thenReturn(user);
// When & Then
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.username").value("testuser"));
verify(userService).createUser(any(User.class));
}
@Test
@DisplayName("Should return 404 when user not found")
void shouldReturn404WhenUserNotFound() throws Exception {
// Given
when(userService.findByUsername("nonexistent")).thenReturn(Optional.empty());
// When & Then
mockMvc.perform(get("/api/users/nonexistent"))
.andExpect(status().isNotFound());
}
@Test
@DisplayName("Should get active users")
void shouldGetActiveUsers() throws Exception {
// Given
User user1 = new User();
user1.setUsername("user1");
User user2 = new User();
user2.setUsername("user2");
when(userService.findActiveUsers()).thenReturn(List.of(user1, user2));
// When & Then
mockMvc.perform(get("/api/users/active"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].username").value("user1"));
}
}
MockMvc:
Simuliert HTTP-Requests ohne echten Server! Schnell und einfach.
jsonPath():
Prüft JSON-Response mit Path-Ausdrücken:
$.username– Feld „username“$[0]– Erstes Array-Element$– Root-Element
🔵 BONUS: API-DOKUMENTATION
📚 Theorie: OpenAPI/Swagger
OpenAPI (früher Swagger) generiert automatisch API-Dokumentation aus deinem Code!
Vorteile:
- ✅ Immer aktuell
- ✅ Interaktiv testbar
- ✅ Standard-Format
- ✅ Client-Code generierbar
Schritt 9: SpringDoc OpenAPI Integration
Dependency hinzufügen:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>
Das war’s! Spring Boot konfiguriert alles automatisch.
Teste es:
# App starten mvn spring-boot:run # Öffne im Browser: http://localhost:8080/swagger-ui.html
Du siehst jetzt eine interaktive API-Dokumentation! 🎉
Schritt 10: Controller dokumentieren
UserController mit Annotations:
package com.example.controller;
import com.example.model.User;
import com.example.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Tag(name = "User Management", description = "APIs für User-Verwaltung")
public class UserController {
private final UserService userService;
@Operation(
summary = "Neuen User erstellen",
description = "Erstellt einen neuen User mit validiertem Username und Email"
)
@ApiResponses(value = {
@ApiResponse(
responseCode = "201",
description = "User erfolgreich erstellt",
content = @Content(schema = @Schema(implementation = User.class))
),
@ApiResponse(
responseCode = "400",
description = "Ungültige Eingabedaten"
),
@ApiResponse(
responseCode = "409",
description = "Username oder Email existiert bereits"
)
})
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User created = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
}
OpenAPI Config (optional):
package com.example.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("User Management API")
.version("1.0.0")
.description("REST API für User-Verwaltung mit Spring Boot")
.contact(new Contact()
.name("Elyndra Valen")
.email("elyndra@javafleet.de")
.url("https://java-developer.online")));
}
}
Schritt 11: TDD – Test-Driven Development
Der TDD-Cycle:
1. RED - Test schreiben (schlägt fehl) 2. GREEN - Code schreiben (Test wird grün) 3. REFACTOR - Code verbessern (Tests bleiben grün)
Praktisches Beispiel:
Phase 1: RED – Test schreiben:
@Test
@DisplayName("Should validate email with subdomains")
void shouldValidateEmailWithSubdomains() {
// Given
User user = new User();
user.setEmail("test@mail.example.com");
// When
boolean result = user.isValidEmail();
// Then
assertThat(result).isTrue();
}
Test ausführen: ❌ Schlägt fehl!
Phase 2: GREEN – Code schreiben:
public boolean isValidEmail() {
return email != null &&
email.matches("^[A-Za-z0-9+_.-]+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,}$");
}
Test ausführen: ✅ Grün!
Phase 3: REFACTOR – Code verbessern:
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,}$");
public boolean isValidEmail() {
if (email == null) {
return false;
}
return EMAIL_PATTERN.matcher(email).matches();
}
Tests ausführen: ✅ Alle grün!
Gewonnen:
- ✅ Performance (Pattern nur 1x kompiliert)
- ✅ Readability
- ✅ Tests garantieren Funktionalität
✅ Checkpoint: Hast du Tag 8 geschafft?
Kontrolliere:
- [ ] JUnit 5 Tests geschrieben
- [ ] Mockito Mocks erstellt
- [ ] @DataJpaTest für Repository genutzt
- [ ] @WebMvcTest für Controller genutzt
- [ ] @SpringBootTest für Integration Tests
- [ ] Test Coverage über 80%
- [ ] OpenAPI/Swagger Dokumentation läuft
- [ ] TDD-Workflow verstanden
Alles ✅? Du bist bereit für Tag 9!
Nicht alles funktioniert?
- Tests laufen nicht? → Prüfe
spring-boot-starter-testDependency - MockMvc funktioniert nicht? →
@WebMvcTestvergessen? - Swagger UI zeigt nichts? → App läuft auf Port 8080?
❓ FAQ (Häufige Fragen)
Q: Wie viel Code-Coverage ist genug?
A: 80% ist ein guter Richtwert. Aber Coverage allein sagt nichts! Wichtiger: Sind die kritischen Pfade getestet?
Q: Muss ich wirklich ALLES testen?
A: Nein! Getter/Setter, simple Constructors – das musst du nicht testen. Konzentriere dich auf Business-Logic.
Q: Unit Tests vs Integration Tests?
A: Beide! Unit Tests sind schnell. Integration Tests sind realistisch. Du brauchst beides.
Q: Wie teste ich Code mit Datenbank?
A: @DataJpaTest für Repository-Tests (H2 in-memory). Nie die Production-DB für Tests nutzen!
Q: Mockito vs echte Dependencies?
A: Kommt drauf an! Unit Tests → Mockito. Integration Tests → echte Dependencies.
Q: TDD – muss ich das machen?
A: Nein, aber es hilft! TDD zwingt zu testbarem Design. Schreib zumindest Tests sofort nach dem Code!
Q: Was macht ihr bei persönlichen Problemen zwischen den Projekten?
A: Das ist… kompliziert. Manche Geschichten gehören nicht in Tech-Blogs, sondern in private logs. Aber das ist ein anderes Kapitel. 🔒
📅 Nächster Kurstag: Tag 9
Morgen im Kurs / Nächster Blogbeitrag:
„Spring Boot Actuator – Production Monitoring“
Was du lernen wirst:
- Health Checks konfigurieren
- Metriken mit Micrometer erfassen
- Custom Actuator Endpoints erstellen
- Prometheus Integration
- Production Monitoring Setup
Dauer: 8 Stunden
Voraussetzung: Tag 8 abgeschlossen
📚 Deine Fortschritts-Übersicht
| Tag | Thema | Status |
|---|---|---|
| ✅ 1 | Auto-Configuration & Starter | ABGESCHLOSSEN! |
| ✅ 2 | Spring Data JPA Basics | ABGESCHLOSSEN! |
| ✅ 3 | JPA Relationships & Queries | ABGESCHLOSSEN! |
| ✅ 4 | Spring Security Part 1 | ABGESCHLOSSEN! |
| ✅ 5 | Spring Security Part 2 | ABGESCHLOSSEN! |
| ✅ 6 | Spring Boot Caching & JSON | ABGESCHLOSSEN! |
| ✅ 7 | Messaging & Email | ABGESCHLOSSEN! |
| ✅ 8 | Testing & Dokumentation | ABGESCHLOSSEN! 🎉 |
| → 9 | Spring Boot Actuator | Als nächstes |
| 10 | Template Engines & Microservices | Noch offen |
Du hast 80% des Kurses geschafft! 💪
📥 Download & Ressourcen
Wichtige Ressourcen zum Testing
- Offizielle Spring Boot Testing-Dokumentation:
„Testing :: Spring Boot“ — beschreibt die grundlegenden Testmodule und wie man mitspring-boot-starter-teststartet. - Tutorial auf Baeldung: „Testing in Spring Boot“ — für Praxisbeispiele mit Unit-, Integrationstests und Testslices.
- Leitfaden zu Best Practices: „Best Practices for How to Test Spring Boot Applications“ — gute Gedanken zur Teststrategie (z. B. FIRST-Prinzipien)
Projekt zum Download:
👉 SpringBootAufbau-Tag8-Testing-v1.0.zip (Stand: 21.10.2025)
Was ist im ZIP enthalten:
- ✅ Komplettes Maven-Projekt mit allen Tests
- ✅ Unit Tests (User, UserRepository, UserService)
- ✅ Integration Tests (@SpringBootTest)
- ✅ Controller Tests (@WebMvcTest)
- ✅ OpenAPI/Swagger Konfiguration
- ✅ H2 Test-Database Setup
- ✅ application-test.properties
- ✅ README mit Schnellstart
- ✅ AUFGABEN.md mit 8-Stunden-Plan
Projekt starten:
# ZIP entpacken # Alle Tests ausführen mvn test # Mit Coverage-Report mvn test jacoco:report # Report: target/site/jacoco/index.html # App starten mvn spring-boot:run # Swagger UI öffnen http://localhost:8080/swagger-ui.html
Probleme? Schreib mir: elyndra@java-developer.online
Das war Tag 8 von Spring Boot Aufbau!
Du kannst jetzt:
- ✅ Unit Tests mit JUnit 5 & Mockito schreiben
- ✅ Repository-Tests mit @DataJpaTest erstellen
- ✅ Service-Tests mit Mocks implementieren
- ✅ Integration-Tests mit @SpringBootTest durchführen
- ✅ Controller-Tests mit MockMvc & @WebMvcTest
- ✅ API-Dokumentation mit OpenAPI/Swagger generieren
- ✅ TDD-Workflow anwenden
- ✅ Test-Coverage messen
Morgen lernst du Spring Boot Actuator für Production Monitoring! 🚀
Keep coding, keep learning! 💙
P.S.: Manchmal verstecken sich die interessantesten Geschichten nicht in Code-Repositories, sondern in den… nun ja, wenn du auf java-developer.online nach „private logs“ suchst, könntest du mehr über Marcus und die Balken erfahren. 😉
Tags: #SpringBoot #Testing #JUnit5 #Mockito #TDD #OpenAPI #Swagger #Tag8

