Testing-Time-Travel Teil 2: Die Test-Pyramide als Physik verstehen

Von Dr. Cassian Holt, Senior Architect & Programming Language Historian bei Java Fleet Systems Consulting in Essen-Rüttenscheid



🎯 Kurze Zusammenfassung – Das Wichtigste in 60 Sekunden

Die Test-Pyramide folgt physikalischen Gesetzen! Unit Tests sind wie Quantenmechanik (isoliert, deterministisch), Integration Tests wie Thermodynamik (Systeme interagieren), E2E Tests wie Astrophysik (komplex, statistisch). Nova’s TaskApp wird zur Test-Architektur mit 70% Unit Tests, 20% Integration Tests, 10% E2E Tests.

Key Learnings:70/20/10-Regel der Test-Pyramide wissenschaftlich erklärt
@SpringBootTest für Integration Tests mit echtem Spring Context
@MockBean für isoliertes Testen von Services
TestRestTemplate für E2E API-Tests

Sofort anwendbar: TaskService mit Integration Tests und TaskController mit E2E Tests!

Challenge-Lösungen aus Teil 1 besprochen + neue Aufgaben für Integration Testing! 🔺


🎉 Challenge-Review: Eure brillanten Lösungen!

Wow! Die Community-Response war überwältigend! 32 Antworten mit euren Challenge-Lösungen – Nova, du warst nicht die einzige, die am Calculator-Test geknobelt hat! 😄

🏆 Highlights aus euren Calculator-Tests:

Besonders elegant war diese Lösung von @JavaNinja_Hamburg:

@ParameterizedTest
@CsvSource({
    "2, 3, 5",      // Positive numbers
    "-2, 3, 1",     // Negative + Positive  
    "-2, -3, -5",   // Both negative
    "0, 5, 5",      // Zero edge case
    "1000000, 1, 1000001"  // Large numbers
})
@DisplayName("Addition should work with all number combinations")
void shouldAddNumbersCorrectly(int a, int b, int expected) {
    Calculator calc = new Calculator();
    
    int result = calc.add(a, b);
    
    assertThat(result).isEqualTo(expected);
}

Brilliant! Ein Test, fünf Szenarien – das ist efficient testing! 🎯

Und @TestingNewbie_Berlin hat das Exception-Testing perfekt gemeistert:

@Test
@DisplayName("Should throw meaningful exception when dividing by zero")
void shouldThrowMeaningfulExceptionOnDivideByZero() {
    Calculator calc = new Calculator();
    
    assertThatThrownBy(() -> calc.divide(10, 0))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessage("Division by zero is not allowed")
        .hasNoCause(); // Keine wrapped exceptions!
}

Das ist wissenschaftlich exaktes Exception-Testing! 🔬

Nova’s TaskValidator-Erweiterung:

Nova, deine Lösung war besonders durchdacht:

@Test
@DisplayName("Should collect multiple validation errors for completely invalid task")
void shouldCollectMultipleValidationErrors() {
    // Arrange - Ein Task mit ALLEN möglichen Fehlern
    String tooLongTitle = "x".repeat(101);
    String tooLongDescription = "x".repeat(501);
    Task invalidTask = new Task(tooLongTitle, tooLongDescription);
    
    // Act
    ValidationResult result = validator.validate(invalidTask);
    
    // Assert - Alle Fehler sollten gesammelt werden
    assertThat(result.isValid()).isFalse();
    assertThat(result.getErrors())
        .hasSize(2)
        .contains("Title too long (max 100 characters)")
        .contains("Description too long (max 500 characters)");
}

Das zeigt wissenschaftliches Denken: Du testest nicht nur einzelne Fehler, sondern multiple Failure-Szenarien gleichzeitig! 🧠

🔍 Die drei Test-Arten verstehen – Bevor wir zur Pyramide kommen

Nova fragte mich nach Teil 1: „Cassian, alle reden von Unit Tests, Integration Tests und E2E Tests. Aber was ist eigentlich der konkrete Unterschied? Wann verwende ich was?“

Excellent question! Bevor wir zur Test-Pyramide als Physik kommen, lass uns die drei Test-Arten mit Nova’s TaskApp verstehen:

🧪 Unit Tests – „Teste eine Sache isoliert“

Was wird getestet: Eine einzelne Klasse oder Methode ohne externe Dependencies

// Unit Test - Testet NUR TaskValidator, nichts anderes!
@Test
@DisplayName("Unit Test: TaskValidator should reject empty title - NO external dependencies")
void shouldRejectEmptyTitle() {
    // Arrange - Nur die Klasse die wir testen
    TaskValidator validator = new TaskValidator(); // Keine Spring Context!
    Task task = new Task("", "Description");      // Keine Database!
    
    // Act - Nur diese eine Methode
    ValidationResult result = validator.validate(task);
    
    // Assert - Nur das Verhalten dieser Klasse
    assertThat(result.isValid()).isFalse();
    assertThat(result.getErrors()).contains("Title cannot be empty");
}
// Wenn dieser Test fehlschlägt, wissen wir: Problem ist in TaskValidator.validate()!

Eigenschaften von Unit Tests:

  • Schnell: <100ms pro Test
  • Isoliert: Keine Database, kein Netzwerk, kein Spring Context
  • Deterministisch: Immer gleiches Ergebnis
  • Präzise: Wenn Test fehlschlägt, weißt du exakt welche Zeile das Problem ist

Nova’s Analogie: „Unit Tests sind wie einzelne Experimente im Labor – ich teste nur eine Variable!“

🔗 Integration Tests – „Teste wie Komponenten zusammenarbeiten“

Was wird getestet: Mehrere Komponenten zusammen, aber nicht das komplette System

// Integration Test - Testet TaskService + TaskRepository + Spring Context zusammen
@SpringBootTest
@Transactional
@Test
@DisplayName("Integration Test: TaskService + Database working together")
void shouldSaveTaskToDatabase() {
    // Arrange - Echte Spring Beans mit echter H2 Database!
    @Autowired TaskService taskService;           // Echter Spring Service
    @Autowired TaskRepository taskRepository;     // Echtes JPA Repository
    @MockBean NotificationService notificationService; // Externe Service gemockt
    
    CreateTaskRequest request = new CreateTaskRequest("Integration Test", "Real DB");
    
    // Act - Testet Service + Validation + Database + Transaction
    TaskResponse response = taskService.createTask(request);
    
    // Assert - Verifiziere Database-Integration
    assertThat(response.getId()).isNotNull();     // Database hat ID generiert
    
    // Verifiziere dass es wirklich in der Database ist
    Optional<Task> saved = taskRepository.findById(response.getId());
    assertThat(saved).isPresent();
    assertThat(saved.get().getTitle()).isEqualTo("Integration Test");
}
// Wenn dieser Test fehlschlägt: Problem könnte in TaskService, Repository, Database-Config sein

Eigenschaften von Integration Tests:

  • Mittel-schnell: 500ms – 2s pro Test
  • 🔗 Mehrere Komponenten: Spring Context + Database + Services
  • 🎭 Externe Services gemockt: NotificationService, EmailService, etc.
  • 🎯 Testet Interaktionen: Wie arbeiten Komponenten zusammen?

Nova’s Analogie: „Integration Tests sind wie Tests zwischen verschiedenen Labor-Geräten – funktionieren sie zusammen?“

🌐 E2E Tests – „Teste das komplette System wie ein echter User“

Was wird getestet: Das komplette System von außen, wie ein echter Benutzer es verwenden würde

// E2E Test - Testet ALLES: HTTP → Security → Controller → Service → Database → HTTP
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Test
@DisplayName("E2E Test: Complete TaskApp workflow like real user")
void shouldCreateTaskViaHTTPLikeRealUser() {
    // Arrange - Echte HTTP Requests wie Frontend oder Mobile App
    @Autowired TestRestTemplate restTemplate;
    
    CreateTaskRequest request = new CreateTaskRequest("E2E Test", "Real HTTP API");
    
    // Act - Kompletter HTTP-Workflow mit Authentication
    ResponseEntity<TaskResponse> response = restTemplate
        .withBasicAuth("nova", "learning123")  // Echte Spring Security!
        .postForEntity("/api/tasks", request, TaskResponse.class);
    
    // Assert - Teste vom User-Standpunkt
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    assertThat(response.getBody().getTitle()).isEqualTo("E2E Test");
    
    // Verifiziere dass der User die Task auch abrufen kann
    ResponseEntity<TaskResponse[]> getAllResponse = restTemplate
        .withBasicAuth("nova", "learning123")
        .getForEntity("/api/tasks", TaskResponse[].class);
    
    assertThat(getAllResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(getAllResponse.getBody()).isNotEmpty();
}
// Wenn dieser Test fehlschlägt: Problem könnte überall sein - HTTP, Security, Controller, Service, Database

Eigenschaften von E2E Tests:

  • 🐌 Langsam: 2s – 10s pro Test
  • 🌐 Komplettes System: Alles wie in Production
  • 👤 User-Perspektive: Wie würde ein echter Benutzer die App verwenden?
  • 🎲 Schwer zu debuggen: Problem könnte überall sein

Nova’s Analogie: „E2E Tests sind wie einen ganzen Patienten untersuchen – nicht nur einzelne Organe!“

🤔 Wann verwende ich welchen Test-Typ?

Nova: „Okay, ich verstehe die Unterschiede. Aber wann soll ich was verwenden?“

Hier ist die praktische Entscheidungsmatrix für Nova’s TaskApp:

Test-TypWann verwenden?Beispiel aus TaskAppWie viele?
Unit TestEinzelne Methode/Klasse testenTaskValidator.validate()Viele (70%)
Integration TestKomponenten-Zusammenarbeit testenTaskService + DatabaseEinige (20%)
E2E TestKompletter User-WorkflowHTTP API Call → ResponseWenige (10%)

Konkrete Entscheidungshilfe:

// Unit Test: Wenn du fragen kannst "Was macht diese eine Methode?"
@Test void shouldValidateTaskTitle() { /* Teste nur Validation-Logic */ }

// Integration Test: Wenn du fragen kannst "Arbeiten diese Services zusammen?"
@SpringBootTest @Test void shouldSaveTaskToDatabase() { /* Teste Service + DB */ }

// E2E Test: Wenn du fragen kannst "Kann ein User das komplette Feature nutzen?"
@Test void shouldCreateTaskViaAPI() { /* Teste kompletten HTTP-Workflow */ }

Nova’s Aha-Moment: „Ah! Unit Tests für die Details, Integration Tests für die Verbindungen, E2E Tests für die User-Experience!“

🔺 Die Test-Pyramide: Es IST tatsächlich Physik!

Nova fragte mich nach Teil 1: „Cassian, warum ist die Test-Pyramide wie Physik? Das verstehe ich noch nicht!“

Hier ist der wissenschaftliche Beweis: Testing-Systeme folgen den gleichen mathematischen Gesetzen wie physikalische Systeme! 🔬

⚛️ Unit Tests = Quantenmechanik

Quantenmechanik-Prinzipien:

  • Isolierte Systeme: Keine äußeren Einflüsse
  • Deterministische Zustandsübergänge: Gleicher Input = gleicher Output
  • Lokale Wechselwirkungen: Nur direkte Dependencies
  • Superposition: Alle möglichen Zustände testbar

Unit Test-Eigenschaften:

@Test
@DisplayName("TaskValidator behaves like quantum mechanics - isolated and deterministic")
void demonstrateQuantumMechanicsInUnitTesting() {
    // ISOLATION - Test läuft in isolierter Umgebung
    TaskValidator validator = new TaskValidator(); // Fresh instance, no dependencies
    
    // DETERMINISTIC - Immer gleiches Ergebnis
    Task task = new Task("Learn Quantum Testing", "Science meets Code");
    ValidationResult result1 = validator.validate(task);
    ValidationResult result2 = validator.validate(task);
    
    assertThat(result1.isValid()).isEqualTo(result2.isValid()); // Deterministic!
    
    // LOCAL INTERACTIONS - Nur direkte Dependencies (keine!)
    assertThat(validator).isNotNull(); // No database, no network, no external state
    
    // SUPERPOSITION - Alle Edge Cases gleichzeitig testbar
    Stream.of("", null, "x".repeat(101), "Valid Title")
        .forEach(title -> {
            Task testTask = new Task(title, "Description");
            ValidationResult result = validator.validate(testTask);
            // Each test is independent and predictable
        });
}

Mathematische Eigenschaften:

  • Ausführungszeit: O(1) – konstant, unabhängig von System-Größe
  • Fehler-Lokalisierung: O(1) – exakt eine Fehlerquelle
  • Parallelisierbarkeit: O(n) – perfekt parallelisierbar
  • Setup-Komplexität: O(1) – minimaler Setup-Aufwand

🌡️ Integration Tests = Thermodynamik

Thermodynamik-Prinzipien:

  • Systeme interagieren: Energieaustausch zwischen Komponenten
  • Entropie steigt: Systeme tendieren zu Unordnung (mehr kann schiefgehen!)
  • Emergente Eigenschaften: Gesamtsystem ≠ Summe der Teile

Integration Test-Eigenschaften:

@SpringBootTest
@TestPropertySource(properties = "spring.profiles.active=test")
class TaskServiceIntegrationTest {
    
    @Autowired
    private TaskService taskService; // System interaction with Spring
    
    @Autowired
    private TaskRepository taskRepository; // Database interaction
    
    @Test
    @DisplayName("TaskService demonstrates thermodynamics - systems interact and create emergent behavior")
    void demonstrateThermodynamicsInIntegrationTesting() {
        // SYSTEM INTERACTIONS - Multiple components exchange "energy" (data)
        CreateTaskRequest request = new CreateTaskRequest("Integration Test", "Spring Context Magic");
        
        // EMERGENT PROPERTIES - Behavior not predictable from individual components
        TaskResponse response = taskService.createTask(request);
        
        // Database generates ID - emergent property!
        assertThat(response.getId()).isNotNull();
        assertThat(response.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now());
        
        // ENTROPY INCREASES - More components = more potential failure points
        // Database could be slow, Spring context could fail, validation could throw...
        // We test the interaction, not just individual parts
        
        // Verify the complete system worked
        Optional<Task> savedTask = taskRepository.findById(response.getId());
        assertThat(savedTask).isPresent();
        assertThat(savedTask.get().getTitle()).isEqualTo("Integration Test");
    }
}

Mathematische Eigenschaften:

  • Ausführungszeit: O(log n) – steigt logarithmisch mit Komponenten-Anzahl
  • Fehler-Lokalisierung: O(log n) – Binary-Search durch Component-Stack
  • Parallelisierbarkeit: O(log n) – begrenzt durch shared resources (Database)
  • Setup-Komplexität: O(n) – linear mit Anzahl Dependencies

🌌 E2E Tests = Astrophysik

Astrophysik-Prinzipien:

  • Komplexe Systeme: Galaxien mit Milliarden von Sternen
  • Chaotische Dynamik: Kleine Änderungen → große Auswirkungen
  • Beobachtungseffekte: Messung beeinflusst System
  • Statistisches Verhalten: Nur probabilistische Vorhersagen möglich

E2E Test-Eigenschaften:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class TaskApiE2ETest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @LocalServerPort
    private int port;
    
    @Test
    @DisplayName("Complete TaskAPI demonstrates astrophysics - complex system with statistical behavior")
    void demonstrateAstrophysicsInE2ETest() {
        // COMPLEX SYSTEMS - Full application stack like a galaxy
        String baseUrl = "http://localhost:" + port;
        
        CreateTaskRequest request = new CreateTaskRequest(
            "E2E Test Task", 
            "Testing the entire universe of TaskApp"
        );
        
        // CHAOTIC DYNAMICS - Small changes can have large effects
        // A slightly different request timing might hit different code paths
        ResponseEntity<TaskResponse> response = restTemplate
            .withBasicAuth("nova", "learning123") // Security layer
            .postForEntity(baseUrl + "/api/tasks", request, TaskResponse.class);
        
        // OBSERVATION EFFECTS - Test execution affects system state  
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        
        // STATISTICAL BEHAVIOR - Can only make probabilistic assertions
        TaskResponse task = response.getBody();
        assertThat(task.getId()).isNotNull(); // We know this will exist
        assertThat(task.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now().plusSeconds(5)); // But timing is statistical
        
        // The system is too complex to predict exact behavior, but we can verify high-level properties
    }
}

Mathematische Eigenschaften:

  • Ausführungszeit: O(n²) – exponentiell mit System-Komplexität
  • Fehler-Lokalisierung: O(n) – linear search durch gesamten Stack
  • Parallelisierbarkeit: O(1) – stark begrenzt durch shared state
  • Setup-Komplexität: O(n²) – quadratisch mit Service-Anzahl

📐 Die 70/20/10-Regel: Mathematisch bewiesen!

Nova: „Warum genau 70% Unit Tests, 20% Integration, 10% E2E? Ist das nur eine Faustregeln oder steckt Wissenschaft dahinter?“

Antwort: Es steckt Mathematik dahinter! Basierend auf den physikalischen Eigenschaften können wir die optimale Verteilung berechnen:

/**
 * PHYSIK-BASIERTE TEST-VERTEILUNGS-FORMEL
 * 
 * Gegeben:
 * - T = Gesamte verfügbare Test-Zeit
 * - C = Code-Komplexität (Lines of Code + Cyclomatic Complexity)
 * - R = Risk-Level (Business-Critical Factor 0.0-1.0)
 * - F = Feedback-Speed-Requirement (wie schnell brauchst du Ergebnisse?)
 * 
 * Optimale Verteilung:
 * Unit Tests:        70% = (T * 0.7) / O(1)     - Konstante Zeit pro Test
 * Integration Tests: 20% = (T * 0.2) / O(log C) - Logarithmisch mit Komplexität  
 * E2E Tests:         10% = (T * 0.1) / O(C²)    - Quadratisch mit Komplexität
 */

@Component
public class TestPyramidCalculator {
    
    public TestDistribution calculateOptimal(
            Duration totalTestTime,
            CodeComplexity complexity,
            double riskLevel) {
        
        // Base distribution (physics-derived)
        double unitRatio = 0.70;
        double integrationRatio = 0.20;
        double e2eRatio = 0.10;
        
        // Risk adjustment (business reality)
        if (riskLevel > 0.8) { // High risk = more E2E
            e2eRatio += 0.05;
            unitRatio -= 0.05;
        } else if (riskLevel < 0.3) { // Low risk = more Unit
            e2eRatio -= 0.03;
            unitRatio += 0.03;
        }
        
        return TestDistribution.builder()
            .unitTests((int)(totalTestTime.toSeconds() * unitRatio))
            .integrationTests((int)(totalTestTime.toSeconds() * integrationRatio))
            .e2eTests((int)(totalTestTime.toSeconds() * e2eRatio))
            .build();
    }
}

Für Nova’s TaskApp bedeutet das:

  • 7 Unit Tests (TaskValidator, individual methods)
  • 2 Integration Tests (TaskService + Database)
  • 1 E2E Test (Complete API workflow)

🏗️ Nova’s TaskApp Integration Tests bauen

Zeit für Praxis! Lass uns Nova’s TaskApp mit der wissenschaftlich korrekten Test-Pyramide ausbauen.

Schritt 1: TaskService erstellen

// src/main/java/com/javafleet/taskmanager/service/TaskService.java
@Service
@RequiredArgsConstructor
@Transactional
public class TaskService {
    
    private final TaskRepository taskRepository;
    private final TaskValidator taskValidator;
    private final NotificationService notificationService; // External dependency!
    
    public TaskResponse createTask(CreateTaskRequest request) {
        // Validate input
        Task task = new Task(request.getTitle(), request.getDescription());
        if (request.getPriority() != null) {
            task.setPriority(request.getPriority());
        }
        
        ValidationResult validation = taskValidator.validate(task);
        if (!validation.isValid()) {
            throw new ValidationException("Invalid task: " + validation.getFirstError());
        }
        
        // Save to database
        Task savedTask = taskRepository.save(task);
        
        // Send notification (external system!)
        try {
            notificationService.sendTaskCreatedNotification(savedTask);
        } catch (Exception e) {
            // Log but don't fail the transaction
            log.warn("Failed to send notification for task {}: {}", savedTask.getId(), e.getMessage());
        }
        
        // Convert to response
        return TaskResponse.builder()
            .id(savedTask.getId())
            .title(savedTask.getTitle())
            .description(savedTask.getDescription())
            .priority(savedTask.getPriority())
            .completed(savedTask.isCompleted())
            .createdAt(savedTask.getCreatedAt())
            .build();
    }
    
    public List<TaskResponse> getAllTasks() {
        return taskRepository.findAll().stream()
            .map(this::convertToResponse)
            .collect(toList());
    }
    
    private TaskResponse convertToResponse(Task task) {
        return TaskResponse.builder()
            .id(task.getId())
            .title(task.getTitle())
            .description(task.getDescription())
            .priority(task.getPriority())
            .completed(task.isCompleted())
            .createdAt(task.getCreatedAt())
            .build();
    }
}

Schritt 2: Integration Test für TaskService

// src/test/java/com/javafleet/taskmanager/service/TaskServiceIntegrationTest.java
@SpringBootTest
@Transactional
@Rollback
class TaskServiceIntegrationTest {
    
    @Autowired
    private TaskService taskService;
    
    @Autowired
    private TaskRepository taskRepository;
    
    @MockBean // This mocks the external dependency!
    private NotificationService notificationService;
    
    @Test
    @DisplayName("Should create task and save to database with all Spring magic")
    void shouldCreateTaskAndSaveToDatabase() {
        // Arrange - Integration test with real Spring context
        CreateTaskRequest request = new CreateTaskRequest(
            "Integration Test Task", 
            "Testing TaskService with real database"
        );
        
        // Act - This goes through: Validation → Database → Response conversion
        TaskResponse response = taskService.createTask(request);
        
        // Assert - Verify the complete flow worked
        assertThat(response.getId()).isNotNull();
        assertThat(response.getTitle()).isEqualTo("Integration Test Task");
        assertThat(response.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now());
        
        // Verify it's actually in the database
        Optional<Task> savedTask = taskRepository.findById(response.getId());
        assertThat(savedTask).isPresent();
        assertThat(savedTask.get().getTitle()).isEqualTo("Integration Test Task");
        
        // Verify external service was called
        verify(notificationService).sendTaskCreatedNotification(any(Task.class));
    }
    
    @Test
    @DisplayName("Should handle validation errors gracefully")
    void shouldHandleValidationErrors() {
        // Arrange - Invalid request
        CreateTaskRequest invalidRequest = new CreateTaskRequest("", "Description");
        
        // Act & Assert - Should throw validation exception
        assertThatThrownBy(() -> taskService.createTask(invalidRequest))
            .isInstanceOf(ValidationException.class)
            .hasMessageContaining("Title cannot be empty");
        
        // Verify no notification was sent for invalid task
        verify(notificationService, never()).sendTaskCreatedNotification(any());
    }
    
    @Test
    @DisplayName("Should continue working even if notification fails")
    void shouldHandleNotificationFailuresGracefully() {
        // Arrange - Mock notification service to fail
        doThrow(new RuntimeException("Notification service down"))
            .when(notificationService).sendTaskCreatedNotification(any());
        
        CreateTaskRequest request = new CreateTaskRequest("Test Task", "Description");
        
        // Act - Should still work despite notification failure
        TaskResponse response = taskService.createTask(request);
        
        // Assert - Task was created despite notification failure
        assertThat(response).isNotNull();
        assertThat(response.getId()).isNotNull();
        
        // Verify task is in database
        assertThat(taskRepository.findById(response.getId())).isPresent();
    }
}

Schritt 3: E2E Test für TaskController

// src/test/java/com/javafleet/taskmanager/controller/TaskApiE2ETest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {
    "spring.datasource.url=jdbc:h2:mem:testdb-e2e",
    "logging.level.org.springframework.web=DEBUG"
})
class TaskApiE2ETest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @LocalServerPort
    private int port;
    
    @MockBean
    private NotificationService notificationService;
    
    private String getBaseUrl() {
        return "http://localhost:" + port;
    }
    
    @Test
    @DisplayName("Should complete full task creation workflow via HTTP API")
    void shouldCompleteFullTaskCreationWorkflow() {
        // Arrange - Complete HTTP request like a real frontend
        CreateTaskRequest request = new CreateTaskRequest(
            "E2E API Test", 
            "Testing complete HTTP → Spring Security → Controller → Service → Database flow"
        );
        
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<CreateTaskRequest> httpEntity = new HttpEntity<>(request, headers);
        
        // Act - Real HTTP call with authentication
        ResponseEntity<TaskResponse> response = restTemplate
            .withBasicAuth("nova", "learning123")
            .exchange(
                getBaseUrl() + "/api/tasks",
                HttpMethod.POST,
                httpEntity,
                TaskResponse.class
            );
        
        // Assert - Complete E2E flow verification
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        
        TaskResponse createdTask = response.getBody();
        assertThat(createdTask).isNotNull();
        assertThat(createdTask.getId()).isNotNull();
        assertThat(createdTask.getTitle()).isEqualTo("E2E API Test");
        assertThat(createdTask.isCompleted()).isFalse();
        assertThat(createdTask.getCreatedAt()).isBeforeOrEqualTo(LocalDateTime.now());
        
        // Verify we can retrieve the task
        ResponseEntity<TaskResponse[]> getAllResponse = restTemplate
            .withBasicAuth("nova", "learning123")
            .getForEntity(getBaseUrl() + "/api/tasks", TaskResponse[].class);
        
        assertThat(getAllResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(getAllResponse.getBody()).isNotEmpty();
        assertThat(Arrays.stream(getAllResponse.getBody()))
            .anyMatch(task -> task.getId().equals(createdTask.getId()));
    }
    
    @Test
    @DisplayName("Should return 401 for unauthenticated requests")
    void shouldReturn401ForUnauthenticatedRequests() {
        // Act - Request without authentication
        CreateTaskRequest request = new CreateTaskRequest("Unauthorized", "Should fail");
        
        ResponseEntity<String> response = restTemplate.postForEntity(
            getBaseUrl() + "/api/tasks", 
            request, 
            String.class
        );
        
        // Assert - Security should block the request
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
    }
    
    @Test
    @DisplayName("Should return 400 for invalid task data")
    void shouldReturn400ForInvalidTaskData() {
        // Arrange - Invalid request (empty title)
        CreateTaskRequest invalidRequest = new CreateTaskRequest("", "Description");
        
        // Act - Request with bad data
        ResponseEntity<String> response = restTemplate
            .withBasicAuth("nova", "learning123")
            .postForEntity(getBaseUrl() + "/api/tasks", invalidRequest, String.class);
        
        // Assert - Validation should catch it
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
    }
}

🎛️ @MockBean vs @Mock: Wann was verwenden?

Nova fragte: „Cassian, ich sehe überall @MockBean und @Mock. Was ist der Unterschied?“

Excellent question! Das ist ein häufiger Verwirrungspunkt:

@Mock (Mockito) – für Unit Tests:

class TaskServiceUnitTest {
    
    @Mock
    private TaskRepository mockRepository; // Pure Mockito mock
    
    @Mock 
    private NotificationService mockNotificationService;
    
    @InjectMocks
    private TaskService taskService; // Mockito injects the mocks
    
    @Test
    void shouldCreateTaskWithMockedDependencies() {
        // This runs WITHOUT Spring context - pure unit test
        when(mockRepository.save(any())).thenReturn(createSavedTask());
        
        TaskResponse response = taskService.createTask(createRequest());
        
        assertThat(response).isNotNull();
        verify(mockRepository).save(any());
    }
}

@MockBean (Spring Boot) – für Integration Tests:

@SpringBootTest
class TaskServiceIntegrationTest {
    
    @Autowired
    private TaskRepository realRepository; // Real Spring bean with H2 database
    
    @MockBean
    private NotificationService mockNotificationService; // Spring mock
    
    @Autowired
    private TaskService taskService; // Real Spring bean with mocked notification
    
    @Test
    void shouldCreateTaskWithRealDatabaseMockedNotification() {
        // This runs WITH Spring context - integration test
        // Database is real, notification is mocked
        
        TaskResponse response = taskService.createTask(createRequest());
        
        assertThat(response.getId()).isNotNull(); // Real database generated ID
        verify(mockNotificationService).sendTaskCreatedNotification(any());
    }
}

Die Regel:

  • Unit Tests: @Mock + @InjectMocks (keine Spring Context)
  • Integration Tests: @MockBean für external dependencies (mit Spring Context)
  • E2E Tests: @MockBean nur für echte externe Services (Emails, etc.)

🎯 Deine Challenge für diese Woche

Zeit für praktische Experimente mit der Test-Pyramide! 🧪

Aufgabe 1: Vollständige Test-Pyramide für Nova’s TaskApp

Baue die 70/20/10-Verteilung für deine TaskApp:

// Unit Tests (70% - mindestens 7 Tests):
TaskValidatorTest.java         // ✅ Haben wir schon aus Teil 1
TaskServiceUnitTest.java       // 🎯 Neu: Teste mit @Mock dependencies
TaskResponseBuilderTest.java   // 🎯 Neu: Teste die Response-Conversion
PriorityEnumTest.java         // 🎯 Neu: Teste Edge Cases der Priority enum

// Integration Tests (20% - 2 Tests):
TaskServiceIntegrationTest.java // 🎯 Mit echtem Spring Context + Database
TaskRepositoryTest.java        // 🎯 @DataJpaTest für Repository-Layer

// E2E Tests (10% - 1 Test):
TaskApiE2ETest.java           // 🎯 Kompletter HTTP-Workflow

Bonus: Miss die tatsächliche Ausführungszeit und vergleiche:

# Unit Tests: ~100ms
# Integration Tests: ~2000ms  
# E2E Tests: ~5000ms

Aufgabe 2: Mocking-Master-Challenge

Teste verschiedene Failure-Szenarien mit Mocks:

@SpringBootTest
class TaskServiceFailureScenarioTest {
    
    @MockBean
    private NotificationService notificationService;
    
    @Test
    @DisplayName("Should handle notification service timeout gracefully")
    void shouldHandleNotificationTimeout() {
        // Mock a timeout scenario
        doThrow(new RuntimeException("Connection timeout after 5 seconds"))
            .when(notificationService).sendTaskCreatedNotification(any());
        
        // Your task: Verify TaskService handles this gracefully
        // Task should still be created and saved!
    }
    
    @Test  
    @DisplayName("Should retry notification on temporary failures")
    void shouldRetryNotificationOnTemporaryFailures() {
        // Advanced: Mock first call fails, second succeeds
        doThrow(new RuntimeException("Temporary failure"))
            .doNothing() // Second call succeeds
            .when(notificationService).sendTaskCreatedNotification(any());
        
        // Your task: Implement and test retry logic
    }
}

Aufgabe 3: E2E Security Testing

Teste Nova’s Security-Integration aus Learning Monday 2:

@SpringBootTest(webEnvironment = RANDOM_PORT)
class TaskSecurityE2ETest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    @DisplayName("Should test complete authentication flow")
    void shouldTestCompleteAuthenticationFlow() {
        // Test 1: No auth = 401
        // Test 2: Wrong credentials = 401  
        // Test 3: Correct credentials = 200
        // Test 4: Session persistence across requests
        
        // Your mission: Implement all security scenarios!
    }
}

📅 Ausblick: Was kommt am Dienstag?

Teil 3: TDD in der Praxis – Red-Green-Refactor als Forschungsmethode! Nova wird ein komplett neues Feature für ihre TaskApp test-first entwickeln. Plus: Property-Based Testing mit jqwik für mathematische Beweise über Code-Verhalten!

Sneak Preview: Wir bauen eine Task-Search-Funktionalität mit TDD und testen sie mit hunderten automatisch generierter Inputs. Spoiler: Es ist wie ein Monte-Carlo-Experiment für Code! 🎲


💬 Community-Diskussion

Wie ist eure Test-Pyramide aufgebaut? Haltet ihr die 70/20/10-Regel ein oder habt ihr andere Erfahrungen gemacht?

Schreibt in die Kommentare:

  • Integration Tests: Lieblingswerkzeug oder notwendiges Übel?
  • Mocking: @MockBean vs @Mock – wo hattet ihr Verwirrung?
  • E2E Tests: Wie testet ihr komplette Workflows ohne Flaky Tests?

Erfahrene Entwickler: Wie erklärt ihr Newbies den Unterschied zwischen Unit- und Integration Tests? Habt ihr gute Analogien?

Challenge-Teilnehmer: Postet eure Lösungen als GitHub PRs – wir besprechen die besten in Teil


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert