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-Typ | Wann verwenden? | Beispiel aus TaskApp | Wie viele? |
---|---|---|---|
Unit Test | Einzelne Methode/Klasse testen | TaskValidator.validate() | Viele (70%) |
Integration Test | Komponenten-Zusammenarbeit testen | TaskService + Database | Einige (20%) |
E2E Test | Kompletter User-Workflow | HTTP API Call → Response | Wenige (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
Schreibe einen Kommentar