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
| Situation | Verwende |
|---|---|
| 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:
| Frage | Wenn 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? | ✅ |
| Frage | Wenn 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

