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


Testing & Dokumentation

📍 Deine Position im Kurs

TagThemaStatus
1Auto-Configuration & Custom StarterAbgeschlossen
2Spring Data JPA BasicsAbgeschlossen
3JPA Relationships & QueriesAbgeschlossen
4Spring Security Part 1 – AuthenticationAbgeschlossen
5Spring Security Part 2 – AuthorizationAbgeschlossen
6Spring Boot Caching & JSONAbgeschlossen
7Messaging & EmailAbgeschlossen
→ 8Testing & Dokumentation👉 DU BIST HIER!
9Spring Boot ActuatorNoch nicht freigeschaltet
10Template Engines & MicroservicesNoch 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 Regex
  • isValidUsername() – Username muss zwischen 3 und 20 Zeichen sein
  • canLogin() – 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-test Dependency
  • MockMvc funktioniert nicht? → @WebMvcTest vergessen?
  • 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

TagThemaStatus
✅ 1Auto-Configuration & StarterABGESCHLOSSEN!
✅ 2Spring Data JPA BasicsABGESCHLOSSEN!
✅ 3JPA Relationships & QueriesABGESCHLOSSEN!
✅ 4Spring Security Part 1ABGESCHLOSSEN!
✅ 5Spring Security Part 2ABGESCHLOSSEN!
✅ 6Spring Boot Caching & JSONABGESCHLOSSEN!
✅ 7Messaging & EmailABGESCHLOSSEN!
✅ 8Testing & DokumentationABGESCHLOSSEN! 🎉
→ 9Spring Boot ActuatorAls nächstes
10Template Engines & MicroservicesNoch offen

Du hast 80% des Kurses geschafft! 💪


📥 Download & Ressourcen

Wichtige Ressourcen zum Testing

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

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.