Von: Jamal Hassan, Backend Developer
Deep-Dive Beiträge: Dr. Cassian Holt, Senior Architect
Java Fleet Systems Consulting, Essen-Rüttenscheid


🔗 Bisher in der Testing-Serie

Prolog:Warum diese Serie anders wird – Cassian & Jamal kündigen ihre Zusammenarbeit an.

Teil 1: Unit Testing Grundlagen (Jamal solo) – Deine ersten Unit-Tests mit AAA-Pattern und AssertJ.

Teil 2: Die Test-Pyramide in der Praxis (beide) – Warum 70% Unit, 20% Integration, 10% E2E.

Teil 3: TDD & Property-Based Testing (beide) – Die kontroverse Diskussion über Theorie vs. Praxis.

Heute: Teil 4 – Mocking & Test Doubles. Warum deine Tests schnell bleiben, auch wenn dein Code komplex wird.


⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden

Mocking bedeutet: Ersetze echte Dependencies (Datenbank, REST-API, Email-Service) durch Fake-Objekte in Tests. Dadurch werden Tests schnell (millisekunden statt sekunden), stabil (keine Netzwerk-Timeouts) und isoliert (ein Test pro Methode). Mockito ist das Standard-Framework für Java. @Mock für Unit-Tests, @MockBean für Spring Integration-Tests. Du definierst Verhalten mit when().thenReturn() und verifizierst Aufrufe mit verify(). Wichtig: Mocke nur direkte Dependencies, nicht alles. Über-Mocking macht Tests fragil. Nach diesem Artikel kannst DU entscheiden, was gemockt werden sollte – und was nicht.


👋 Moin! Jamal hier

Heute geht’s um Mocking. Und ich weiß, das klingt erstmal abstrakt.

Aber hier ist die Wahrheit: Ohne Mocking sind deine Tests die Hölle.

Lass mich dir meine Geschichte erzählen.


😱 Mein Test-Alptraum (Vor Mocking)

Das Projekt: Order-Management-System
Das Problem: Alle Tests reden mit echten Services

@SpringBootTest
class OrderServiceTest {
    
    @Autowired
    private OrderService orderService;
    
    // Echte Datenbank! 
    // Echter Email-Service!
    // Echter Payment-Gateway!
    
    @Test
    void shouldCreateOrder() {
        // Dieser Test:
        // - Braucht laufende Datenbank (PostgreSQL)
        // - Sendet echte Emails (an test@example.com)
        // - Ruft echten Payment-Gateway auf (Sandbox)
        // - Dauert 8 Sekunden pro Test
        // - Bricht random (Netzwerk-Timeout)
        
        Order order = orderService.createOrder(createRequest());
        
        assertThat(order.getId()).isNotNull();
    }
}

Das Ergebnis:

  • ❌ 150 Tests = 20 Minuten Laufzeit
  • ❌ Tests brechen random (flaky)
  • ❌ CI/CD Pipeline ständig rot
  • ❌ Niemand will Tests schreiben
  • ❌ „Ich teste lokal“ wird zur Ausrede

Dann habe ich Mocking gelernt.


✅ Nach Mocking: Das gleiche Feature, 100x schneller

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    
    @Mock
    private OrderRepository orderRepository;
    
    @Mock
    private EmailService emailService;
    
    @Mock
    private PaymentGateway paymentGateway;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    void shouldCreateOrder() {
        // Setup Mocks
        when(orderRepository.save(any(Order.class)))
            .thenAnswer(invocation -> {
                Order order = invocation.getArgument(0);
                order.setId(123L);
                return order;
            });
        
        when(paymentGateway.processPayment(any()))
            .thenReturn(PaymentResult.success("TXN_999"));
        
        // Test
        Order order = orderService.createOrder(createRequest());
        
        // Verify
        assertThat(order.getId()).isEqualTo(123L);
        verify(emailService).sendConfirmation(any());
    }
}

Das Ergebnis:

  • ✅ 150 Tests = 8 Sekunden Laufzeit
  • ✅ Keine flaky Tests mehr
  • ✅ CI/CD immer grün
  • ✅ Team schreibt gerne Tests
  • ✅ Schnelles Feedback

Das ist der Unterschied.


🤔 Was ist Mocking überhaupt?

Einfach erklärt:

Du hast eine Klasse, die andere Klassen braucht:

public class OrderService {
    
    private OrderRepository repository;      // Datenbank
    private EmailService emailService;       // Email-Server
    private PaymentGateway paymentGateway;   // Externes API
    
    public Order createOrder(CreateOrderRequest request) {
        // 1. Validiere Payment
        PaymentResult payment = paymentGateway.processPayment(request);
        
        // 2. Speichere Order
        Order order = new Order(request.getTitle());
        order.setPaymentId(payment.getTransactionId());
        Order saved = repository.save(order);
        
        // 3. Sende Email
        emailService.sendConfirmation(saved);
        
        return saved;
    }
}

Problem ohne Mocking:

Um OrderService zu testen, brauchst du:

  • ❌ Laufende Datenbank
  • ❌ Email-Server (oder Fake-SMTP)
  • ❌ Payment-Gateway (Sandbox oder Produktion)

Lösung mit Mocking:

Ersetze repository, emailService, paymentGateway durch Fake-Objekte, die:

  • ✅ Sofort antworten (keine Netzwerk-Calls)
  • ✅ Das zurückgeben, was du ihnen sagst
  • ✅ Aufzeichnen, was aufgerufen wurde

🎭 Mockito: Dein Mocking-Framework

Setup

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <!-- Enthält bereits Mockito! -->
</dependency>

Die 3 Schritte des Mockings

1. Mock erstellen

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    
    @Mock
    private PaymentGateway paymentGateway;  // Fake PaymentGateway
    
    @InjectMocks
    private OrderService orderService;      // Echte OrderService mit Mocks
}

2. Verhalten definieren: when().thenReturn()

@Test
void shouldProcessPayment() {
    // Sage dem Mock, was er zurückgeben soll
    when(paymentGateway.processPayment(any()))
        .thenReturn(PaymentResult.success("TXN_123"));
    
    // Jetzt rufe deine Methode auf
    Order order = orderService.createOrder(createRequest());
    
    // Mock hat "TXN_123" zurückgegeben
    assertThat(order.getPaymentId()).isEqualTo("TXN_123");
}

3. Aufrufe verifizieren: verify()

@Test
void shouldSendConfirmationEmail() {
    when(paymentGateway.processPayment(any()))
        .thenReturn(PaymentResult.success("TXN_123"));
    
    orderService.createOrder(createRequest());
    
    // Wurde sendConfirmation() aufgerufen?
    verify(emailService).sendConfirmation(any(Order.class));
}

🔧 Praktische Beispiele

Beispiel 1: Happy Path

@Test
void shouldCreateOrderSuccessfully() {
    // Arrange - Mocks vorbereiten
    CreateOrderRequest request = new CreateOrderRequest("Test Order", 99.99);
    
    when(paymentGateway.processPayment(any()))
        .thenReturn(PaymentResult.success("TXN_999"));
    
    when(orderRepository.save(any(Order.class)))
        .thenAnswer(invocation -> {
            Order order = invocation.getArgument(0);
            order.setId(123L);
            return order;
        });
    
    // Act - Methode aufrufen
    Order result = orderService.createOrder(request);
    
    // Assert - Ergebnis prüfen
    assertThat(result.getId()).isEqualTo(123L);
    assertThat(result.getPaymentId()).isEqualTo("TXN_999");
    
    // Verify - Aufrufe prüfen
    verify(emailService).sendConfirmation(result);
}

Beispiel 2: Fehlerfall

@Test
void shouldHandlePaymentFailure() {
    // Arrange - Mock wirft Exception
    when(paymentGateway.processPayment(any()))
        .thenThrow(new PaymentException("Card declined"));
    
    // Act & Assert - Exception erwartet
    assertThatThrownBy(() -> orderService.createOrder(createRequest()))
        .isInstanceOf(OrderCreationException.class)
        .hasMessageContaining("Payment failed");
    
    // Verify - Kein Email bei Fehler
    verify(emailService, never()).sendConfirmation(any());
}

Beispiel 3: Mehrere Szenarien

@Test
void shouldHandleDifferentPaymentAmounts() {
    // Unterschiedliches Verhalten je nach Input
    when(paymentGateway.processPayment(argThat(req -> 
        req.getAmount() > 1000)))
        .thenReturn(PaymentResult.success("HIGH_VALUE_TXN"));
    
    when(paymentGateway.processPayment(argThat(req -> 
        req.getAmount() <= 1000)))
        .thenReturn(PaymentResult.success("NORMAL_TXN"));
    
    // Test High Value
    Order highValue = orderService.createOrder(
        new CreateOrderRequest("Expensive", 1500.0));
    assertThat(highValue.getPaymentId()).isEqualTo("HIGH_VALUE_TXN");
    
    // Test Normal Value
    Order normalValue = orderService.createOrder(
        new CreateOrderRequest("Cheap", 50.0));
    assertThat(normalValue.getPaymentId()).isEqualTo("NORMAL_TXN");
}

🆚 @Mock vs @MockBean: Der große Unterschied

Das verwirrt jeden am Anfang. Hier ist die Klarstellung:

@Mock (Mockito)

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    
    @Mock
    private PaymentGateway paymentGateway;
    
    @InjectMocks
    private OrderService orderService;
    
    // KEIN Spring Context!
    // Sehr schnell (< 100ms)
    // Nur für Unit-Tests
}

Wann nutzen?

  • ✅ Reine Unit-Tests
  • ✅ Teste eine Klasse isoliert
  • ✅ Keine Spring-Features nötig
  • ✅ Maximale Geschwindigkeit

@MockBean (Spring Boot)

@SpringBootTest
class OrderServiceIntegrationTest {
    
    @Autowired
    private OrderService orderService;  // Echter Spring Bean
    
    @MockBean
    private PaymentGateway paymentGateway;  // Mock im Spring Context
    
    // MIT Spring Context!
    // Langsamer (2-5 Sekunden)
    // Für Integration-Tests
}

Wann nutzen?

  • ✅ Integration-Tests
  • ✅ Teste mit Spring Context
  • ✅ Brauchst @Autowired, @Transactional, etc.
  • ✅ Teste Service-Layer mit echten Beans

Die Faustregel

SituationVerwende
Teste eine Klasse isoliert@Mock
Teste Service + Repository@MockBean
Brauchst Spring-Features@MockBean
Maximale Geschwindigkeit@Mock
Teste Controller@MockBean + MockMvc

⚠️ Häufige Fehler beim Mocking

Fehler 1: Alles mocken

// ❌ SCHLECHT
@Mock
private String title;  // Warum?!

@Mock
private LocalDateTime createdAt;  // Warum?!

@Mock
private BigDecimal amount;  // Warum?!

// ✅ GUT - Value Objects nicht mocken!
String title = "Real Title";
LocalDateTime createdAt = LocalDateTime.now();
BigDecimal amount = new BigDecimal("99.99");

Regel: Mocke nur Dependencies (Services, Repositories), nicht Value Objects (String, LocalDateTime, etc.)


Fehler 2: Die getestete Klasse mocken

// ❌ SCHLECHT - WTF?!
@Mock
private OrderService orderService;

@Test
void shouldCreateOrder() {
    when(orderService.createOrder(any()))
        .thenReturn(new Order());
    
    Order order = orderService.createOrder(createRequest());
    // Du testest NICHTS! Der Mock gibt zurück, was du sagst!
}

// ✅ GUT - Teste echte Klasse
@InjectMocks
private OrderService orderService;  // ECHTE Klasse mit gemockten Dependencies

Fehler 3: Zu spezifische Mocks

// ❌ SCHLECHT - Zu spezifisch
when(paymentGateway.processPayment(
    argThat(req -> 
        req.getAmount().equals(new BigDecimal("99.99")) &&
        req.getCurrency().equals("EUR") &&
        req.getUserId() == 123L &&
        req.getDescription().equals("Test Order"))))
    .thenReturn(success());

// Test bricht bei kleinster Änderung!

// ✅ GUT - Flexibel
when(paymentGateway.processPayment(any(PaymentRequest.class)))
    .thenReturn(success());

Regel: Mocke flexibel, nicht brittle.


Fehler 4: Über-Verifizierung

// ❌ SCHLECHT - Jede Kleinigkeit verifizieren
@Test
void shouldCreateOrder() {
    orderService.createOrder(createRequest());
    
    verify(logger).debug("Starting order creation");
    verify(validator).validate(any());
    verify(logger).debug("Validation passed");
    verify(paymentGateway).processPayment(any());
    verify(logger).info("Payment successful");
    verify(repository).save(any());
    verify(logger).debug("Order saved");
    verify(emailService).sendConfirmation(any());
    verify(logger).info("Email sent");
    // Test bricht bei jeder Änderung im Logging!
}

// ✅ GUT - Nur wichtige Interaktionen
@Test
void shouldCreateOrder() {
    orderService.createOrder(createRequest());
    
    verify(paymentGateway).processPayment(any());
    verify(repository).save(any());
    verify(emailService).sendConfirmation(any());
    // Nur Business-Logic, kein Logging!
}

Regel: Verifiziere nur geschäftskritische Aufrufe.


📊 Meine Mocking-Checkliste

Bevor ich einen Mock erstelle, frage ich mich:

FrageWenn Ja → Mock it
Ist es eine externe Dependency? (DB, API, Service)
Würde der Test sonst langsam werden? (> 100ms)
Würde der Test sonst flaky werden? (Netzwerk)
Teste ich nur eine Klasse isoliert?
FrageWenn Ja → NICHT mocken
Ist es ein Value Object? (String, LocalDateTime)
Ist es die getestete Klasse selbst?
Ist es triviale Logik? (Getter/Setter)

🎯 Praktische Guidelines

Was ich immer mocke:

Datenbank-Repositories

@Mock
private OrderRepository orderRepository;

Externe Services

@Mock
private PaymentGateway paymentGateway;

@Mock
private EmailService emailService;

Alles was Netzwerk-Calls macht

@Mock
private RestTemplate restTemplate;

@Mock
private WebClient webClient;

Was ich NIE mocke:

Value Objects

// NICHT mocken!
String title = "Real String";
LocalDateTime now = LocalDateTime.now();
BigDecimal amount = new BigDecimal("99.99");

Die getestete Klasse

// NICHT mocken!
@InjectMocks
private OrderService orderService;  // Echte Instanz!

Triviale Dependencies

// NICHT mocken - zu simpel!
private OrderValidator validator = new OrderValidator();

<details> <summary>📚 <strong>Cassian’s Deep-Dive: Die Theorie der Test Doubles</strong></summary>

Mocking ist eine Spezialisierung des Test Double Patterns von Gerard Meszaros (2007).

Die 5 Arten von Test Doubles:

1. Dummy – Objekt, das nur als Parameter existiert

public void sendEmail(String to, EmailConfig config) {
    // config wird nie benutzt
}

// Test:
EmailConfig dummy = new EmailConfig();  // Dummy!
emailService.sendEmail("test@example.com", dummy);

2. Stub – Gibt vordefinierte Antworten

class StubPaymentGateway implements PaymentGateway {
    public PaymentResult processPayment(PaymentRequest req) {
        return PaymentResult.success("STUB_TXN");  // Immer Erfolg
    }
}

3. Spy – Echtes Objekt + Aufzeichnung

class SpyEmailService extends EmailService {
    List<String> sentEmails = new ArrayList<>();
    
    @Override
    public void send(String email) {
        sentEmails.add(email);  // Aufzeichnen
        super.send(email);      // Dann echter Call
    }
}

4. Mock – Erwartungen + Verifikation (Mockito)

@Mock
private PaymentGateway gateway;

when(gateway.processPayment(any())).thenReturn(success());
verify(gateway).processPayment(any());

5. Fake – Vereinfachte Implementierung

class FakeDatabase implements Database {
    private Map<Long, Order> orders = new HashMap<>();
    
    public Order save(Order order) {
        order.setId(System.currentTimeMillis());
        orders.put(order.getId(), order);
        return order;
    }
}

In der Praxis:

  • Mockito erstellt Mocks (und Spies)
  • Du schreibst selten manuelle Stubs/Fakes
  • Aber das Verständnis hilft bei komplexen Fällen!

</details>


💡 Advanced: ArgumentCaptor

Manchmal willst du prüfen, was genau an einen Mock übergeben wurde:

@Test
void shouldPassCorrectDataToPaymentGateway() {
    // Arrange
    ArgumentCaptor<PaymentRequest> captor = 
        ArgumentCaptor.forClass(PaymentRequest.class);
    
    when(paymentGateway.processPayment(any()))
        .thenReturn(PaymentResult.success("TXN"));
    
    // Act
    orderService.createOrder(
        new CreateOrderRequest("Test", 99.99));
    
    // Capture & Assert
    verify(paymentGateway).processPayment(captor.capture());
    
    PaymentRequest captured = captor.getValue();
    assertThat(captured.getAmount()).isEqualByComparingTo(new BigDecimal("99.99"));
    assertThat(captured.getCurrency()).isEqualTo("EUR");
}

Wann nutzen?

  • ✅ Wenn du komplexe Objekte prüfen musst
  • ✅ Wenn verify() nicht reicht
  • ⚠️ Aber nicht übertreiben – macht Tests komplex

🗓️ Nächste Woche: Integration Testing

Vorschau auf Teil 5:

„Integration Testing mit Spring Boot – Testcontainers & @DataJpaTest“

Du lernst:

  • @SpringBootTest richtig nutzen
  • Testcontainers für echte Databases
  • @DataJpaTest für Repository-Tests
  • Wann Integration-Tests sinnvoll sind

Cassian: Integration-Tests sind die Brücke zwischen Unit und E2E.

Jamal: Und ich zeige dir, wie du sie schreibst, ohne dass deine Test-Suite zur Schnecke wird.


❓ FAQ – Mocking Edition

Frage 1: Wann @Mock, wann @MockBean?
Jamal: Unit-Test ohne Spring? → @Mock. Integration-Test mit Spring? → @MockBean. Faustregel: Wenn du @Autowired brauchst, nutze @MockBean.

Frage 2: Muss ich wirklich ALLES mocken?
Jamal: NEIN! Mocke nur externe Dependencies (DB, API, Services). Value Objects (String, LocalDateTime) NIE mocken. Die getestete Klasse NIE mocken.

Frage 3: Meine Mocks machen Tests kompliziert. Normal?
Jamal: Wenn du 20 Zeilen Mock-Setup für 2 Zeilen Test hast, ist was faul. Entweder: (1) Deine Klasse hat zu viele Dependencies (Bad Design), oder (2) Du mockst zu viel (Over-Mocking).

Frage 4: Wie viele verify() pro Test?
Jamal: Max 2-3! Verifiziere nur geschäftskritische Calls. Nicht jede Kleinigkeit (Logging, Validierung). Über-Verifizierung macht Tests fragil.

Frage 5: Mock vs. echte Datenbank für Integration-Tests?
Jamal: Für Integration-Tests: Echte Datenbank (H2, Testcontainers)! Mocks sind für Unit-Tests. Integration-Tests sollen echtes Zusammenspiel testen.

Frage 6: Kann man zu viel mocken?
Jamal: JA! Wenn 90% deines Codes gemockt ist, testest du nichts Echtes mehr. Balance ist key. Unit-Tests: Viel mocken. Integration-Tests: Wenig mocken.

Frage 7: Cassian, warum 5 Arten von Test Doubles?
Cassian: Historisch gewachsen aus verschiedenen Use Cases. Aber in der Praxis: Mockito macht fast alles. Die Theorie hilft bei Edge Cases.

Frage 8: Wie geht ihr mit Mocking-Frustration um?
Jamal: Früher habe ich jeden Test mit 50 Zeilen Mock-Setup angefangen. Heute: Wenn Mock-Setup kompliziert wird, ist das ein Code-Smell. Refactore deine Klasse! Und manchmal… ist Code einfacher als Leben. Private logs, anyone? 🔒


📖 Testing-Serie – Alle Teile

✅ Bereits veröffentlicht:

  • Prolog: Warum diese Serie anders wird
  • Teil 1: Unit Testing Grundlagen (Jamal solo)
  • Teil 2: Test-Pyramide in der Praxis (beide)
  • Teil 3: TDD & Property-Based Testing (beide)
  • Teil 4 (heute): Mocking & Test Doubles

📅 Kommende Teile:

  • Teil 5: Integration Testing mit Spring Boot (Jamal solo)
  • Teil 6: Deine Testing-Strategie (beide)

💭 Schlusswort

Jamal: Mocking hat meine Test-Suite von 20 Minuten auf 8 Sekunden gebracht. Das ist kein Scherz.

Aber: Mocking ist ein Werkzeug, kein Ziel. Mocke, was du musst. Nicht, was du kannst.

Die wichtigste Lektion:

  • Langsame Tests = Niemand führt sie aus
  • Schnelle Tests = Team nutzt sie täglich
  • Mocking = Schnelle Tests

Also: Lerne mocking. Nutze es klug. Und deine Tests werden dir danken.


Zwischen den Zeilen: Manchmal wünschte ich, ich könnte auch im Leben einfach @Mock über Probleme schreiben und sie verschwinden lassen. Aber so funktioniert es nicht. Manche Dinge kann man nicht mocken… nur durchleben. Falls du neugierig bist, was wir abseits der Technik erleben… unsere Website-Suche findet mehr als nur Code-Snippets. Probier mal „herzschmerz“ – nur so als Tipp! 🔍


Keep coding, keep testing! 🚀

Nächste Woche: Integration Testing – Endlich echte Databases in deinen Tests (mit Testcontainers)!


Jamal’s Motto:

„Mocke nur, was du musst. Nicht, was du kannst.“ 🎭


Jamal Hassan – Backend Developer & Spring Boot Specialist
Java Fleet Systems Consulting, Essen-Rüttenscheid, Oktober 2025

6 Jahre Erfahrung. Von 20-Min-Tests zu 8-Sek-Tests. Dank Mocking.


Tags: #Testing #Mocking #Mockito #MockBean #UnitTests #SpringBoot #TestDoubles #RealWorldTesting

Autor

  • Cassian Holt

    43 Jahre alt, promovierter Informatiker mit Spezialisierung auf Programming Language Theory. Cassian arbeitet als Senior Architect bei Java Fleet Systems Consulting und bringt eine einzigartige wissenschaftliche Perspektive in praktische Entwicklungsprojekte. Seine Leidenschaft: Die Evolution von Programmiersprachen und warum "neue" Features oft alte Konzepte sind.