Von Dr. Cassian Holt, Senior Architect & Programming Language Historian bei Java Fleet Systems Consulting in Essen-Rüttenscheid
🔗 Bisher in der Testing-Time-Travel-Serie:
- Testing Glossar
- Teil 1: Die Evolution des Testens – JUnit 3→4→5 Geschichte, Testing-Basics
- Teil 2: Die Wissenschaft der Test-Pyramide – Unit/Integration/E2E als Physik
- Teil 3: Property-Based Testing & TDD-Kulturschock – jqwik + Entwicklungs-Revolution
Heute: Teil 4 – Mocking & Test Doubles Deep-Dive
📝 Kurze Zusammenfassung
🎭 Test Doubles & Mocking:
- Was sind Test Doubles und warum brauchen wir sie?
- Mockito von Grund auf – das Standard-Mocking-Framework
- Rückblick: Wo wir schon unbewusst gemockt haben
- Spring Boot @MockBean vs. klassisches Mocking
🔍 Bonus-Thema: Contract Testing:
- Consumer-Driven Contracts mit Pact
- API-Verträge zwischen Services testen
🎯 Diese Woche: Von isolierten Unit Tests zu vertrauenswürdigen Service-Verträgen!
🎭 Der große Mocking-Rückblick: „Ups, haben wir schon die ganze Zeit gemockt!“
Dr. Cassian hier – und heute wird’s interessant! Nova kam gestern zu mir: „Cassian, in deinen Beispielen verwendest du dauernd @MockBean
und @Mock
– aber was ist das eigentlich genau?“
Gute Frage! 🤔 Beim Durchblick durch unsere Serie fiel mir auf: Wir haben schon in Teil 2 und 3 gemockt, ohne es richtig zu erklären!
Zeit für eine ehrliche Aufarbeitung – heute holen wir alles nach! 🧹
🔍 Wo wir schon heimlich gemockt haben:
Teil 2 (Test-Pyramide):
// Das hier war schon Mocking! @SpringBootTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) class TaskServiceIntegrationTest { @MockBean // ← MOCKING! Haben wir nicht erklärt private EmailService emailService; @Test void shouldCreateTaskWithoutSendingEmail() { // Implizites Mocking - EmailService tut nichts } }
Teil 3 (Property-Based Testing):
// Auch das war Mocking! @ExtendWith(MockitoExtension.class) // ← MOCKING-Framework! class PropertyBasedSecurityTest { @Mock // ← Klassisches Mock-Objekt private UserRepository userRepository; // Haben wir verwendet, aber nicht erklärt! }
Nova’s Reaktion: „Oh wow, ich dachte das wären einfach nur Annotations!“ 😅
🎭 Test Doubles: Die Schauspieler deiner Tests
📚 Die Theorie: Gerard Meszaros‘ Test Double Patterns
Wie im Film: Manchmal braucht man Stunt-Doubles statt echter Schauspieler!
// Das Problem ohne Test Doubles: @Test void shouldProcessPayment() { PaymentService service = new PaymentService(); // 😱 Das würde echtes Geld überweisen! // 😱 Braucht echte Datenbank! // 😱 Braucht Internet-Verbindung! // 😱 Dauert 5 Sekunden! PaymentResult result = service.processPayment(request); }
🎯 Die 5 Arten von Test Doubles:
// 1. DUMMY - Objekt das nur da ist, aber nie verwendet wird class DummyEmailService implements EmailService { public void sendEmail(String to, String subject, String body) { // Tut nichts - ist nur Parameter-Filler } } // 2. STUB - Gibt vordefinierte Antworten zurück class StubPaymentGateway implements PaymentGateway { public PaymentResult process(PaymentRequest request) { // Immer erfolgreich - vordefinierte Antwort return PaymentResult.success("STUB_12345"); } } // 3. SPY - Echtes Objekt, aber zeichnet Aufrufe auf class SpyAuditLogger extends AuditLogger { private List<String> recordedLogs = new ArrayList<>(); @Override public void log(String message) { recordedLogs.add(message); // Aufzeichnung super.log(message); // Dann echter Aufruf } public List<String> getRecordedLogs() { return recordedLogs; } } // 4. MOCK - Vordefinierte Erwartungen + Verifikation // (Wird von Framework wie Mockito erstellt) // 5. FAKE - Vereinfachte, funktionierende Implementierung class FakeUserRepository implements UserRepository { private Map<Long, User> users = new HashMap<>(); public User save(User user) { user.setId(System.currentTimeMillis()); // Einfache ID-Generation users.put(user.getId(), user); return user; } public Optional<User> findById(Long id) { return Optional.ofNullable(users.get(id)); } }
🔧 Mockito Deep-Dive: Das Schweizer Taschenmesser des Mockings
🚀 Mockito Setup und Basics
// Maven Dependency <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.8.0</version> <scope>test</scope> </dependency> // Mockito in JUnit 5 integrieren @ExtendWith(MockitoExtension.class) class PaymentServiceTest { // 3 Wege, Mocks zu erstellen: // 1. Mit Annotation (empfohlen) @Mock private PaymentGateway paymentGateway; // 2. Programmatisch im Test @Test void manualMockCreation() { PaymentGateway gateway = Mockito.mock(PaymentGateway.class); } // 3. Mit @InjectMocks für die getestete Klasse @InjectMocks private PaymentService paymentService; // Bekommt automatisch alle @Mock-Objekte injiziert }
🎯 Mock-Verhalten konfigurieren: when().thenReturn()
@Test void shouldHandleSuccessfulPayment() { // ARRANGE: Mock-Verhalten definieren PaymentRequest request = createValidPaymentRequest(); PaymentResult expectedResult = PaymentResult.success("TXN_12345"); when(paymentGateway.processPayment(request)) .thenReturn(expectedResult); // ACT: Getestete Methode aufrufen PaymentResult actualResult = paymentService.processPayment(request); // ASSERT: Ergebnis prüfen assertThat(actualResult) .isEqualTo(expectedResult) .extracting(PaymentResult::getTransactionId) .isEqualTo("TXN_12345"); } @Test void shouldHandlePaymentFailure() { // Mock kann auch Exceptions werfen when(paymentGateway.processPayment(any(PaymentRequest.class))) .thenThrow(new PaymentGatewayException("Network timeout")); assertThatThrownBy(() -> paymentService.processPayment(createValidPaymentRequest())) .isInstanceOf(PaymentProcessingException.class) .hasMessageContaining("Gateway error"); }
🔍 Mock-Verifikation: verify() – „Wurde aufgerufen?“
@Test void shouldLogAuditEventAfterPayment() { // Arrange PaymentRequest request = createValidPaymentRequest(); when(paymentGateway.processPayment(request)) .thenReturn(PaymentResult.success("TXN_999")); // Act paymentService.processPayment(request); // Assert - Verifikation der Interaktionen verify(auditLogger).log("Payment processed: TXN_999"); verify(auditLogger, times(1)).log(anyString()); // Erweiterte Verifikationen verify(emailService, never()).sendEmail(anyString(), anyString(), anyString()); verify(paymentGateway, atLeastOnce()).processPayment(request); } @Test void shouldCallServicesInCorrectOrder() { // Act paymentService.processPayment(createValidPaymentRequest()); // Verify order of calls InOrder inOrder = inOrder(validator, paymentGateway, auditLogger); inOrder.verify(validator).validate(any(PaymentRequest.class)); inOrder.verify(paymentGateway).processPayment(any(PaymentRequest.class)); inOrder.verify(auditLogger).log(anyString()); }
🎨 Advanced Mocking Patterns
class AdvancedMockingTest { @Test void shouldHandleComplexMockingScenarios() { // Conditional Mocking - abhängig vom Input when(paymentGateway.processPayment(argThat(request -> request.getAmount().compareTo(new BigDecimal("1000")) > 0))) .thenReturn(PaymentResult.success("HIGH_VALUE_TXN")); when(paymentGateway.processPayment(argThat(request -> request.getAmount().compareTo(new BigDecimal("1000")) <= 0))) .thenReturn(PaymentResult.success("NORMAL_TXN")); // Callback-basiertes Mocking when(paymentGateway.processPayment(any())) .thenAnswer(invocation -> { PaymentRequest request = invocation.getArgument(0); return PaymentResult.success("CALLBACK_" + request.getUserId()); }); } @Test void shouldCaptureMockArguments() { // Argument Captors - fange übergebene Parameter ab ArgumentCaptor<PaymentRequest> requestCaptor = ArgumentCaptor.forClass(PaymentRequest.class); paymentService.processPayment(createValidPaymentRequest()); verify(paymentGateway).processPayment(requestCaptor.capture()); PaymentRequest capturedRequest = requestCaptor.getValue(); assertThat(capturedRequest.getAmount()) .isEqualByComparingTo(new BigDecimal("299.99")); assertThat(capturedRequest.getCurrency()).isEqualTo("EUR"); } @Test @DisplayName("🎭 Should handle partial mocking with spy") void shouldUseSpyForPartialMocking() { // Spy - teilweise echtes Objekt, teilweise gemockt List<String> realList = new ArrayList<>(); List<String> spyList = spy(realList); // Normale Methoden funktionieren wie gewohnt spyList.add("item1"); spyList.add("item2"); assertThat(spyList).hasSize(2); // Aber wir können spezielle Methoden mocken when(spyList.size()).thenReturn(100); assertThat(spyList.size()).isEqualTo(100); // Gemockt assertThat(spyList.get(0)).isEqualTo("item1"); // Echter Aufruf } }
🌱 Spring Boot Mocking: @MockBean vs. @Mock
🤝 Integration Testing mit @MockBean
@SpringBootTest class TaskServiceIntegrationTest { @Autowired private TaskService taskService; // Echter Spring Bean @MockBean // Spring Boot Mock - ersetzt Bean im ApplicationContext private EmailService emailService; @MockBean private AuditService auditService; @Test void shouldCreateTaskWithSpringContext() { // Spring Boot startet (fast) vollständig, aber mit gemockten Beans Task task = new Task("Integration Test Task"); when(emailService.isValidEmail(anyString())).thenReturn(true); Task savedTask = taskService.createTask(task); assertThat(savedTask.getId()).isNotNull(); verify(auditService).logTaskCreated(savedTask.getId()); // EmailService wurde durch Mock ersetzt - kein echter Email-Versand! } }
📊 @Mock vs. @MockBean Entscheidungsmatrix
Kriterium | @Mock (Mockito) | @MockBean (Spring Boot) |
---|---|---|
Speed | ⚡ Sehr schnell | 🐌 Langsamer (Spring Context) |
Scope | Unit Test | Integration Test |
Dependencies | Nur Mockito | Spring Boot + Mockito |
Use Case | Isolierte Klassen-Tests | Service-Layer mit Spring |
Setup | Manuell Dependencies injizieren | Automatisch durch Spring |
// Wann @Mock verwenden: @ExtendWith(MockitoExtension.class) class PaymentCalculatorTest { @Mock private TaxService taxService; // Reine Unit-Tests @Mock private DiscountService discountService; @InjectMocks private PaymentCalculator calculator; // Schnell, isoliert, keine Spring-Dependencies } // Wann @MockBean verwenden: @SpringBootTest class OrderProcessingIntegrationTest { @Autowired private OrderService orderService; // Echter Spring Bean @MockBean private PaymentGateway paymentGateway; // Mock im Spring Context // Langsamer, aber testet echte Spring-Integration }
🏆 Mocking Best Practices: Was Nova gelernt hat
✅ Do’s – Die goldenen Mocking-Regeln
class GoodMockingExamples { @Test @DisplayName("✅ Mock only direct dependencies") void shouldMockOnlyDirectDependencies() { // Gut: Nur direkte Dependencies der getesteten Klasse mocken @Mock PaymentGateway gateway; // PaymentService → PaymentGateway @Mock AuditLogger logger; // PaymentService → AuditLogger // Schlecht wäre: Interne Details des PaymentGateway zu mocken // @Mock HttpClient httpClient; // Das ist PaymentGateway's Problem! } @Test @DisplayName("✅ Use meaningful mock data") void shouldUseMeaningfulTestData() { // Gut: Realistische Test-Daten when(paymentGateway.processPayment(any())) .thenReturn(PaymentResult.builder() .transactionId("TXN_20241201_001") .status(PaymentStatus.SUCCESS) .processedAt(LocalDateTime.now()) .build()); // Schlecht: Bedeutungslose Dummy-Daten // .thenReturn(PaymentResult.builder().transactionId("test").build()); } @Test @DisplayName("✅ Verify important interactions") void shouldVerifyImportantInteractions() { paymentService.processPayment(createHighValuePayment()); // Gut: Geschäftskritische Interaktionen verifizieren verify(auditLogger).logHighValueTransaction(anyString()); verify(fraudDetectionService).checkTransaction(any()); // Schlecht: Jede einzelne Interaktion verifizieren // verify(logger, times(1)).log(anyString()); // Over-verification! } }
❌ Don’ts – Typische Mocking-Fallen
class BadMockingExamples { @Test @DisplayName("❌ Don't mock value objects") void dontMockValueObjects() { // Schlecht: Value Objects sollten NICHT gemockt werden // @Mock LocalDateTime mockTime; // @Mock BigDecimal mockAmount; // @Mock String mockString; // Gut: Echte Value Objects verwenden LocalDateTime realTime = LocalDateTime.now(); BigDecimal realAmount = new BigDecimal("99.99"); String realString = "Real String"; } @Test @DisplayName("❌ Don't mock the class under test") void dontMockClassUnderTest() { // Schlecht: Die getestete Klasse selbst mocken // PaymentService mockPaymentService = mock(PaymentService.class); // when(mockPaymentService.processPayment(any())).thenReturn(result); // Das testet nichts! // Gut: Echte Instanz mit gemockten Dependencies PaymentService realService = new PaymentService(mockGateway, mockLogger); } @Test @DisplayName("❌ Don't over-specify mocks") void dontOverSpecifyMocks() { // Schlecht: Zu spezifische Mock-Konfiguration when(gateway.processPayment( argThat(req -> req.getAmount().equals(new BigDecimal("99.99")) && req.getCurrency().equals("EUR") && req.getUserId() == 12345L))) .thenReturn(successResult()); // Gut: Flexible Mock-Konfiguration when(gateway.processPayment(any(PaymentRequest.class))) .thenReturn(successResult()); } }
🎯 Der „Tell, Don’t Ask“ Trick für Mocks
class TellDontAskMockingTest { @Test @DisplayName("🎯 Focus on behavior, not state") void shouldFocusOnBehaviorNotState() { // Tell-Don't-Ask: Was PASSIERT ist wichtig, nicht WAS zurückgegeben wird PaymentRequest vipCustomerRequest = createVipCustomerRequest(); paymentService.processPayment(vipCustomerRequest); // Fokus: Verhalten verifizieren (was wurde aufgerufen?) verify(vipNotificationService).notifyVipPayment(vipCustomerRequest); verify(loyaltyService).addPoints(eq(vipCustomerRequest.getUserId()), gt(100)); // Nicht so wichtig: Exakte Return-Values // (solange das Verhalten stimmt) } }
🤝 Bonus-Thema: Contract Testing – Mocks die halten, was sie versprechen
Das Problem mit traditionellen Mocks:
// Dieser Mock lügt! 😱 @Test void traditionalMockLiesAboutAPI() { when(paymentService.processPayment(any())) .thenReturn(PaymentResult.success("fake-id")); // Was ist, wenn die echte PaymentService API sich ändert? // Was ist, wenn sie jetzt PaymentException wirft? // Was ist, wenn das Response-Format anders ist? // Mock weiß nichts davon - Test bleibt grün! 💚 // Production bricht! 💥 }
🤝 Consumer-Driven Contracts mit Pact
// Maven Dependencies <dependency> <groupId>au.com.dius.pact.consumer</groupId> <artifactId>junit5</artifactId> <version>4.4.5</version> <scope>test</scope> </dependency> // Consumer Test - Frontend definiert Contract @ExtendWith(PactConsumerTestExt.class) @PactTestFor(providerName = "payment-service") class PaymentServiceContractTest { @Pact(consumer = "task-frontend") public RequestResponsePact createPaymentContract(PactDslWithProvider builder) { return builder .given("user has valid payment method") .uponReceiving("payment request") .path("/api/payments") .method("POST") .headers(Map.of("Content-Type", "application/json")) .body(newJsonBody(payment -> { payment.numberType("amount", 99.99); payment.stringType("currency", "EUR"); payment.numberType("userId", 12345); }).build()) .willRespondWith() .status(201) .headers(Map.of("Content-Type", "application/json")) .body(newJsonBody(response -> { response.stringType("transactionId", "TXN_12345"); response.stringValue("status", "SUCCESS"); response.datetime("processedAt", "yyyy-MM-dd'T'HH:mm:ss"); }).build()) .toPact(); } @Test @PactTestFor(pactMethod = "createPaymentContract") void shouldCreatePaymentAccordingToContract(MockServer mockServer) { // Frontend Code gegen Contract-Mock testen PaymentClient client = new PaymentClient(mockServer.getUrl()); PaymentRequest request = PaymentRequest.builder() .amount(new BigDecimal("99.99")) .currency("EUR") .userId(12345L) .build(); PaymentResult result = client.processPayment(request); // Contract enforcement - diese Struktur MUSS stimmen! assertThat(result.getTransactionId()).isNotNull(); assertThat(result.getStatus()).isEqualTo("SUCCESS"); assertThat(result.getProcessedAt()).isNotNull(); } }
🛡️ Provider Verification – Backend hält Contract ein
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Provider("payment-service") @PactFolder("pacts") class PaymentServiceProviderTest { @LocalServerPort private int port; @BeforeEach void setUp(PactVerificationContext context) { context.setTarget(new HttpTestTarget("localhost", port)); } @State("user has valid payment method") void userHasValidPaymentMethod() { // Test-State einrichten createUserWithValidPaymentMethod(12345L); } @TestTemplate @ExtendWith(PactVerificationInvocationContextProvider.class) void pactVerificationTestTemplate(PactVerificationContext context) { // Verifiziert: Backend erfüllt Frontend-Contract! context.verifyInteraction(); } }
Der Contract-Testing Vorteil:
// Contract Testing verhindert diese Situation: // Frontend erwartet: { "transactionId": "string", "status": "SUCCESS" } // Backend liefert: { "id": 12345, "result": "OK" } // ↑ Contract-Test würde fehlschlagen! ⚠️ // Traditionelle Mocks würden das nicht erkennen!
💡 Action Items für Mocking & Contract Testing
🎯 Level 1: Mocking Basics (diese Woche)
- @Mock und @MockBean in einem bestehenden Test verwenden
- when().thenReturn() für Happy Path implementieren
- verify() für wichtige Interaktionen einbauen
- ArgumentCaptor für komplexe Parameter-Prüfung ausprobieren
🎯 Level 2: Advanced Mocking (nächste Woche)
- @Spy für Partial Mocking verwenden
- Custom ArgumentMatcher schreiben
- InOrder für Aufruf-Reihenfolge prüfen
- @InjectMocks korrekt konfigurieren
🎯 Level 3: Contract Testing (nächster Monat)
- Pact zu Maven/Gradle hinzufügen
- Consumer-Contract für wichtigste API schreiben
- Provider-Verification implementieren
- Breaking Changes durch Contract-Tests verhindern
🎯 Level 4: Production-Ready Mocking (Quartalsziel)
Challenge: „Zero-Surprise API Changes“
- Alle kritischen Service-Interfaces mit Contract Tests absichern
- Mock-Strategy für jedes Team-Projekt definieren
- CI/CD Pipeline mit Contract-Verification einrichten
🎉 Fazit: Mocking ist eine Kunst, kein Zufall!
Was wir heute gelernt haben:
🎭 Test Doubles = Die 5 Schauspieler deiner Tests
- Dummy, Stub, Spy, Mock, Fake – jeder hat seinen Platz
- Mockito macht Mock-Erstellung trivial
- @Mock vs. @MockBean – wähle das richtige Tool
🔧 Mockito Mastery
- when().thenReturn() für Verhalten definieren
- verify() für Interaktions-Prüfung
- ArgumentCaptor für komplexe Assertions
🤝 Contract Testing = Mocks die nicht lügen
- Consumer-Driven Contracts mit Pact
- API-Verträge automatisch verifizieren
- Breaking Changes verhindern BEVOR sie Production erreichen
Für Nova und alle Lernenden:
Mocking ist kein „Schummeln“! Es ist professionelle Test-Isolation. Aber: Mock nur was du musst, nicht was du kannst. Und: Contract-Tests verhindern die „funktioniert bei mir“-Momente!
Für alle Team-Leads:
Invest in Contract Testing! Die 2-3 Stunden Setup sparen euch Wochen von „Warum ist Production kaputt?“-Debugging.
Wie Nova nach dem Mocking-Deep-Dive sagte: „Jetzt verstehe ich endlich, warum meine Tests so schnell sind – sie reden gar nicht mit der echten Datenbank!“ 🚀
📝 Kurze Zusammenfassung
🎭 Test Doubles & Mocking:
- 5 Arten: Dummy, Stub, Spy, Mock, Fake – jede für verschiedene Szenarien
- Mockito als Standard-Framework für Java-Mocking
- @Mock für Unit Tests, @MockBean für Spring Integration Tests
🤝 Contract Testing:
- Consumer-Driven Contracts mit Pact für Java
- API-Verträge zwischen Services automatisch verifizieren
- Breaking Changes verhindern bevor sie Production erreichen
🎯 Nächste Woche: Security-Testing & Legacy Code – Code Sentinel zeigt Security-Automation!
❓ FAQ – Mocking & Contract Testing
Frage 1: Wann verwende ich @Mock und wann @MockBean?
Antwort: @Mock für reine Unit Tests (schnell, isoliert), @MockBean für Spring Integration Tests (langsamer, aber mit echtem Spring Context). Faustregel: Brauchst du Spring-Features? → @MockBean. Ansonsten → @Mock.
Frage 2: Soll ich jeden Service-Call mocken?
Antwort: Nein! Mock nur direkte Dependencies der getesteten Klasse. Interne Details der Dependencies sind deren Problem. Über-Mocking macht Tests brittle und schwer wartbar.
Frage 3: Wie viele verify()-Aufrufe sind okay?
Antwort: Verifiziere nur geschäftskritische Interaktionen. Nicht jede Zeile Code! Rule of Thumb: Max 2-3 verify() pro Test. Mehr deutet auf zu komplexe Tests hin.
Frage 4: Contract Testing vs Integration Testing – was ist der Unterschied?
Antwort: Contract Testing prüft API-Kompatibilität zwischen Services. Integration Tests prüfen vollständige Workflows. Contract Tests sind schneller und finden Breaking Changes früher.
Frage 5: Kann ich Contract Testing für Legacy-APIs verwenden?
Antwort: Ja! Starte mit Charakterisierungs-Contracts – dokumentiere das aktuelle API-Verhalten. Dann schrittweise echte Contracts einführen. Golden Master + Pact = perfekte Legacy-Strategy.
Dr. Cassian Holt verbindet historische Testing-Evolution mit modernen Mocking-Strategien. Nova’s Mocking-Journey zeigt: Von unbewusster Verwendung zu bewusster Meisterschaft!
Tags: #Mocking #TestDoubles #Mockito #SpringBoot #ContractTesting #Pact #UnitTesting #IntegrationTesting
0 Kommentare