Hauptautor: Jamal Hassan, Backend Developer
Deep-Dive Beiträge: Dr. Cassian Holt, Senior Architect
Java Fleet Systems Consulting, Essen-Rüttenscheid
⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden
Die Test-Pyramide (70% Unit, 20% Integration, 10% E2E) ist keine beliebige Empfehlung, sondern optimiert für Speed, Maintenance und Confidence. Unit-Tests laufen in Millisekunden und zeigen präzise, WO ein Fehler ist. Integration-Tests prüfen das Zusammenspiel echter Komponenten. E2E-Tests validieren das Gesamtsystem aus User-Sicht. Die Verteilung folgt einem klaren Prinzip: Je höher in der Pyramide, desto langsamer, teurer und fragiler die Tests. Du lernst heute: Wie du deine Test-Suite optimal aufbaust, wann du welchen Test-Typ brauchst, und wie du die Balance in Spring Boot umsetzt.
👋 Moin! Jamal hier
Letzte Woche haben wir über Unit-Tests gesprochen. Heute geht’s um die große Frage: Wie viele Tests von welchem Typ brauche ich?
Und ja, Cassian hat mir gestern eine 30-minütige Präsentation über „Quantenmechanik in Unit-Tests“ gehalten. Ich hab’s auf 10 Minuten zusammengefasst. Ihr könnt mir später danken. 😄
Aber: Cassian’s Perspektive ist wertvoll. Deshalb findet ihr in diesem Artikel Deep-Dive Boxen von ihm – für alle, die tiefer einsteigen wollen. Ihr könnt sie lesen oder überspringen. Eure Wahl.
🔺 Die Test-Pyramide: Warum 70/20/10?
Vor drei Jahren hatte ich ein Projekt mit dieser Test-Verteilung:
/\
/ \ 50% E2E Tests (😱)
/____\
/ \ 30% Integration Tests
/ \
/__________\
/ \ 20% Unit Tests
/______________/
Das Ergebnis:
- ❌ Test-Suite lief 45 Minuten
- ❌ Tests brachen random (flaky)
- ❌ Niemand wusste, WO der Fehler war
- ❌ Keiner wollte mehr Tests schreiben
Dann haben wir auf die klassische Pyramide umgestellt:
/\
/ \ 10% E2E Tests
/____\
/ \ 20% Integration Tests
/ \
/__________\
/ \ 70% Unit Tests
/______________/
Das Ergebnis:
- ✅ Test-Suite lief in 8 Minuten
- ✅ Tests waren stabil
- ✅ Fehler sofort lokalisierbar
- ✅ Team schreibt gerne Tests
Das ist der Unterschied.
<details> <summary>📚 <strong>Cassian’s Deep-Dive: Die mathematischen Grundlagen der Test-Pyramide</strong></summary>
Die Test-Pyramide ist keine Konvention – sie folgt mathematischen Prinzipien:
Unit-Tests (70%):
- Ausführungszeit: O(1) – konstant, unabhängig von System-Größe
- Fehler-Lokalisierung: O(1) – Test zeigt exakte Methode
- Parallelisierbarkeit: O(n) – perfekt parallelisierbar
Integration-Tests (20%):
- Ausführungszeit: O(log n) – logarithmisch mit System-Komplexität
- Fehler-Lokalisierung: O(log n) – mehrere Komponenten betroffen
- Setup-Komplexität: O(n) – jede Komponente braucht Konfiguration
E2E-Tests (10%):
- Ausführungszeit: O(n²) – quadratisch mit Feature-Anzahl
- Fehler-Lokalisierung: O(n) – alles könnte das Problem sein
- Flaky-Wahrscheinlichkeit: 1/n – je komplexer, desto instabiler
Die 70/20/10-Verteilung optimiert die Funktion:
TestOptimierung = f(Geschwindigkeit, Wartbarkeit, Confidence)
Für ein System mit 100 Features ist diese Verteilung nachweislich optimal. </details>
🟢 Level 1: Unit Tests (70%) – Die Basis
Was sind Unit-Tests?
Tests für eine einzelne Klasse oder eine einzelne Methode. Keine Datenbank, keine Services, keine Network-Calls. Pure Logik.
Warum 70%?
Weil sie:
- In Millisekunden laufen (50-100ms pro Test)
- Nie „flaky“ sind (immer gleich)
- Sofort zeigen, WO der Fehler ist
- Parallel laufen können (alle auf einmal)
- Einfach zu warten sind
Mein Real-World Beispiel:
// Unit Test - läuft in 2ms
@Test
void shouldCalculateDiscountCorrectly() {
// Arrange
OrderCalculator calculator = new OrderCalculator();
// Act
double result = calculator.calculateDiscount(100.0, 0.1);
// Assert
assertThat(result).isEqualTo(90.0);
}
Kein Spring Boot. Keine Datenbank. Nur Java-Objekte.
Das ist, warum du 70% davon willst – sie sind schnell, stabil, und präzise.
Wann schreibe ich Unit-Tests?
Meine Checkliste aus Teil 1:
| Frage | Wenn Ja → Unit Test |
|---|---|
| Hat die Methode Business-Logik (if, switch)? | ✅ |
| Macht sie Berechnungen? | ✅ |
| Validiert sie Input? | ✅ |
| Hat sie mehrere Pfade (Error/Success)? | ✅ |
Was ich NICHT mit Unit-Tests teste:
❌ Framework-Code (Spring, Hibernate)
❌ Simple Getter/Setter
❌ Datenbank-Queries (das sind Integration-Tests)
❌ REST-APIs (das sind Integration/E2E-Tests)
🟠 Level 2: Integration Tests (20%) – Das Zusammenspiel
Was sind Integration-Tests?
Tests für mehrere Komponenten zusammen. Deine Service-Klasse + echte Datenbank. Oder dein Controller + echter Service.
Warum nur 20%?
Weil sie:
- Langsamer sind (500-2000ms pro Test)
- Komplexeres Setup brauchen
- Manchmal fragil sind (DB-Locks, Timing)
- Schwerer zu debuggen sind
Aber: Sie testen Dinge, die Unit-Tests nicht können!
Mein Real-World Beispiel:
// Integration Test - läuft in 500ms
@SpringBootTest
@Transactional
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldSaveOrderToDatabase() {
// Arrange
CreateOrderRequest request = new CreateOrderRequest("Test Order");
// Act
Order order = orderService.createOrder(request);
// Assert
assertThat(order.getId()).isNotNull(); // DB generiert ID!
Order saved = orderRepository.findById(order.getId()).orElseThrow();
assertThat(saved.getTitle()).isEqualTo("Test Order");
}
}
Mit Spring Boot. Mit echter Datenbank (H2). Mit echtem Repository.
Wann schreibe ich Integration-Tests?
| Scenario | Brauche ich einen Integration-Test? |
|---|---|
| Service ruft Repository auf | ✅ |
| Service ruft anderen Service auf | ✅ |
| Controller → Service → Repository | ✅ |
| REST-Call zu externem Service | ✅ (mit WireMock) |
| Email wird versendet | ✅ (mit Mock SMTP) |
Meine Golden Rule:
Wenn zwei oder mehr Spring Beans zusammenarbeiten → Integration Test.
<details> <summary>📚 <strong>Cassian’s Deep-Dive: Warum Integration-Tests wie Thermodynamik sind</strong></summary>
Integration-Tests folgen Prinzipien der Thermodynamik:
1. Systeminteraktion:
Komponenten beeinflussen sich gegenseitig. Service A ruft Service B auf, der die Datenbank modifiziert, was Cache C invalidiert.
2. Emergente Eigenschaften:
Dinge, die nur im Zusammenspiel entstehen:
- Datenbank generiert IDs (Unit-Test kann das nicht prüfen)
- Transaktionen werden committet oder rolled back
- Caches werden befüllt
3. Entropie-Zunahme:
Je mehr Komponenten, desto mehr kann schiefgehen:
- 2 Komponenten = 1 Interaktion
- 3 Komponenten = 3 Interaktionen
- 4 Komponenten = 6 Interaktionen
- Komplexität wächst exponentiell!
4. Energieerhaltung:
Mehr Realität = langsamerer Test. Du tauschst Speed gegen Confidence.
Das ist, warum wir nur 20% Integration-Tests wollen – die Komplexität rechtfertigt nicht mehr. </details>
🔵 Level 3: E2E Tests (10%) – Das Gesamtsystem
Was sind E2E (End-to-End) Tests?
Tests für das komplette System aus User-Perspektive. Frontend, Backend, Datenbank, Services – alles läuft echt.
Warum nur 10%?
Weil sie:
- Sehr langsam sind (5-30 Sekunden pro Test)
- Oft „flaky“ sind (random failing)
- Schwer zu debuggen sind
- Teuer im Setup sind (Docker, Browser, etc.)
Aber: Sie sind die einzigen Tests, die beweisen, dass das System wirklich funktioniert!
Mein Real-World Beispiel:
// E2E Test - läuft in 15 Sekunden
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class OrderE2ETest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15");
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCompleteFullOrderWorkflow() {
// 1. Create Order (wie echter User)
CreateOrderRequest request = new CreateOrderRequest("E2E Order");
ResponseEntity<Order> createResponse = restTemplate.postForEntity(
"/api/orders",
request,
Order.class
);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
// 2. Get Order
String orderId = createResponse.getBody().getId();
ResponseEntity<Order> getResponse = restTemplate.getForEntity(
"/api/orders/" + orderId,
Order.class
);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
// 3. Update Order
UpdateOrderRequest updateRequest = new UpdateOrderRequest("Updated");
restTemplate.put("/api/orders/" + orderId, updateRequest);
// 4. Verify Update
Order updated = restTemplate.getForObject(
"/api/orders/" + orderId,
Order.class
);
assertThat(updated.getTitle()).isEqualTo("Updated");
}
}
Alles echt: Echter HTTP-Server, echte Postgres-Datenbank, echter Request-Flow.
Wann schreibe ich E2E-Tests?
Nur für kritische User-Flows:
✅ Login → Dashboard → Aktion
✅ Checkout-Prozess (Warenkorb → Bezahlung → Bestätigung)
✅ Registrierung → Email-Verifizierung → Login
✅ Daten-Upload → Verarbeitung → Download
Nicht für alles! E2E-Tests sind teuer. Nutze sie nur für das Wichtigste.
<details> <summary>📚 <strong>Cassian’s Deep-Dive: E2E Tests und Chaostheorie</strong></summary>
E2E-Tests folgen Prinzipien chaotischer Systeme:
1. Schmetterlingseffekt:
Kleine Änderungen haben große Auswirkungen:
- 10ms mehr Network-Latency → Test Timeout
- Anderer Test läuft parallel → Database-Lock
- Browser-Update → UI-Element nicht mehr klickbar
2. Sensitivität auf Anfangsbedingungen:
Tests brechen aus scheinbar zufälligen Gründen:
- „Funktioniert auf meinem Rechner“ ≠ „Funktioniert in CI“
- Timing-Issues sind unvorhersehbar
- Race-Conditions treten sporadisch auf
3. Nichtlinearität:
Doppelt so viele E2E-Tests ≠ doppelt so lange Test-Suite:
- Tests beeinflussen sich gegenseitig (Shared State)
- Database-Locks blockieren parallele Ausführung
- Komplexität wächst quadratisch: O(n²)
4. Beobachtungseffekt:
Der Test verändert das System:
- Testdaten beeinflussen echte Daten
- Performance-Profile ändern sich
- Caches verhalten sich anders
Das ist, warum wir nur 10% E2E-Tests wollen – die Kosten rechtfertigen nicht mehr. </details>
📊 Meine Test-Pyramide für Spring Boot Projekte
So sieht meine typische Test-Suite aus:
Projekt: Order-Management-System (mittelgroß)
Gesamt: 150 Tests
/\
/10\ 15 E2E Tests (10%)
/_____\ ├─ Full checkout flow
/ \ ├─ User registration
/ 30 \ └─ Admin workflows
/__________\
/ \ 30 Integration Tests (20%)
/ 110 \ ├─ Service + Repository
/_______________\ ├─ Controller + Service
└─ External API calls
110 Unit Tests (70%)
├─ Business Logic (50 Tests)
├─ Validation (30 Tests)
├─ Calculations (20 Tests)
└─ Edge Cases (10 Tests)
Test-Laufzeit:
├─ Unit: 5 Sekunden
├─ Integration: 2 Minuten
└─ E2E: 5 Minuten
═══════════════════
Total: ~7 Minuten
Vor der Pyramide: 45 Minuten
Nach der Pyramide: 7 Minuten
Gewinn: 38 Minuten pro Build!
🛠️ Praktische Umsetzung in Spring Boot
1. Unit Tests Setup
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
// Kein @SpringBootTest! Nur pure Java
class OrderCalculatorTest {
private OrderCalculator calculator = new OrderCalculator();
@Test
void shouldCalculateTotal() {
double total = calculator.calculateTotal(100.0, 0.19);
assertThat(total).isEqualTo(119.0);
}
}
2. Integration Tests Setup
<!-- pom.xml -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
// MIT @SpringBootTest - startet Spring Context
@SpringBootTest
@Transactional
class OrderServiceIntegrationTest {
@Autowired
private OrderService orderService;
@Test
void shouldCreateOrder() {
Order order = orderService.createOrder(new CreateOrderRequest("Test"));
assertThat(order.getId()).isNotNull();
}
}
3. E2E Tests Setup
<!-- pom.xml -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
// MIT Testcontainers - echte Database in Docker
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class OrderE2ETest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15");
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCompleteWorkflow() {
// Full HTTP requests wie echter User
}
}
✅ Deine Checkliste: Welchen Test-Typ brauche ich?
Frag dich:
| Frage | Test-Typ |
|---|---|
| Teste ich Business-Logik einer einzelnen Methode? | 🟢 Unit |
| Teste ich Berechnungen ohne Dependencies? | 🟢 Unit |
| Teste ich Validation-Rules? | 🟢 Unit |
| Teste ich Service + Repository zusammen? | 🟠 Integration |
| Teste ich Controller + Service? | 🟠 Integration |
| Rufe ich externe APIs auf? | 🟠 Integration (mit Mocks) |
| Teste ich kompletten User-Flow? | 🔵 E2E |
| Teste ich kritischen Business-Prozess? | 🔵 E2E |
🎯 Deine Action Items für diese Woche
Level 1: Analyse (heute)
- Zähle deine aktuellen Tests: Wie viele Unit/Integration/E2E?
- Berechne die Prozente: Passt es zur Pyramide?
- Messe Test-Laufzeiten: Wie lange dauert jeder Typ?
Level 2: Optimierung (diese Woche)
- Identifiziere „falsche“ Tests:
- E2E-Tests, die auch Unit-Tests sein könnten?
- Integration-Tests ohne echten Mehrwert?
- Schreibe 5 neue Unit-Tests für kritische Business-Logik
- Konsolidiere E2E-Tests: Weniger, aber bessere
Level 3: Team-Level
- Dokumentiere deine Test-Strategie
- Teile die Pyramide mit dem Team
- Definiere: Wann schreiben wir welchen Test-Typ?
💬 Community-Challenge
Teile deine Test-Pyramide!
Schick mir eine Mail mit:
- Screenshot deiner aktuellen Test-Verteilung
- Größtes Learning beim Optimieren
- Vorher/Nachher Test-Laufzeiten
Die besten Transformationen feature ich in Teil 3!
🗓️ Nächste Woche: Integration Testing Deep-Dive
Vorschau auf Teil 3:
„Integration Testing in der Praxis – Testcontainers, @MockBean und mehr“
Du lernst:
- @SpringBootTest richtig verwenden
- Testcontainers für echte Databases
- @MockBean vs. @Mock vs. echte Beans
- REST-API Tests mit TestRestTemplate
- External API Mocking mit WireMock
❓ FAQ – Test-Pyramide Edition
Frage 1: Muss ich exakt 70/20/10 einhalten?
Jamal: Nein! Das ist eine Richtlinie, kein Gesetz. Ich habe Projekte mit 80/15/5 und andere mit 60/30/10. Wichtig ist das Prinzip: Mehr Unit, weniger Integration, minimal E2E.
Frage 2: Was wenn mein Projekt hauptsächlich API-Integration ist?
Jamal: Dann hast du vielleicht 50/40/10. Mehr Integration-Tests sind okay, wenn das dein Core-Business ist. Aber versuche trotzdem, so viel wie möglich als Unit-Tests zu isolieren.
Cassian: Die Pyramide passt sich dem Kontext an. Bei Microservices-Heavy Systems ist 60/35/5 oft realistischer. Die mathematischen Prinzipien bleiben, aber die Gewichtung verschiebt sich.
Frage 3: E2E-Tests sind bei uns super flaky. Was tun?
Jamal: Das ist normal! Meine Tipps:
- Reduziere E2E-Tests auf absolute Kern-Flows
- Nutze robuste Waits (nicht
Thread.sleep()) - Isoliere Testdaten besser
- Verwende Retry-Mechanismen für bekannte Flakes
- Akzeptiere 5-10% Flaky-Rate als normal
Frage 4: Wie teste ich Legacy-Code ohne Tests?
Jamal: Start mit E2E-Tests! Ja, das klingt kontraintuitiv, aber:
- E2E-Test für Haupt-Flow (dokumentiert IST-Zustand)
- Dann Integration-Tests für Module
- Dann Unit-Tests beim Refactoring
- Pyramide entsteht von oben nach unten!
Frage 5: Unit-Tests fühlen sich wie Zeitverschwendung an.
Jamal: Ich kenne das Gefühl! Aber rechne mal:
- 10 Min Unit-Test schreiben heute
- Spart 2h Debugging nächste Woche
- Spart 5h Regression-Testing nächsten Monat
- ROI nach 2 Wochen!
Cassian: Unit-Tests sind wie Zinseszins – der Wert akkumuliert exponentiell über Zeit.
Frage 6: Wie überzeuge ich mein Team von mehr Unit-Tests?
Jamal: Zeigen, nicht predigen!
- Nimm ein Feature
- Schreib es mit guten Unit-Tests
- Zeig die Vorteile: Schnell, stabil, einfach zu ändern
- Lass sie selbst refactoren mit dem Safety-Net
- Sie werden konvertieren! 😄
Frage 7: Was ist mit der Test-Pyramide in Microservices?
Jamal: Da wird’s komplizierter! Contract-Tests kommen dazu, Service-Virtualisierung… Das ist Material für einen eigenen Artikel.
Cassian: Die Pyramide bleibt gültig, aber jeder Microservice hat seine eigene. Plus: Contract-Tests zwischen Services als neue Dimension.
Frage 8: Wie geht ihr mit persönlichen Herausforderungen um?
Jamal: Manchmal debugge ich lieber Code als mein eigenes Leben. Manche Geschichten gehören nicht in Tech-Blogs, sondern in private logs. 🔒
Cassian: Das Leben folgt nicht immer mathematischen Gesetzen. Manchmal braucht man mehr als Logik…
📖 Testing-Serie – Alle Teile
✅ Bereits veröffentlicht:
- Testing-Time-Travel: Prolog – Warum diese Serie anders wird
- Teil 1: Unit Testing Grundlagen (Jamal solo)
- Teil 2 (heute): Test-Pyramide in der Praxis
📅 Kommende Teile:
- Teil 3: Integration Testing Deep-Dive (Jamal solo)
- Teil 4: TDD – Die kritische Diskussion (beide)
- Teil 5: Property-Based Testing (Cassian solo)
- Teil 6: Mutation Testing (Cassian solo)
💭 Schlusswort
Jamal: Die Test-Pyramide ist keine Raketenwissenschaft. Sie ist praktische Optimierung: Viele schnelle Tests, wenige langsame Tests, minimal komplexe Tests.
Cassian: Und doch folgt sie mathematischen Prinzipien, die universell gelten. Die Eleganz liegt in der Einfachheit.
Jamal: Seh ich’s richtig, dass du gerade meine Pragmatismus-Rede wissenschaftlich validiert hast?
Cassian: Möglicherweise.
Jamal: Touché.
Zwischen den Zeilen: Während ich über Test-Optimierung schreibe, denke ich manchmal, dass ich mein eigenes Leben auch optimieren sollte. Weniger Zeit auf Dating-Apps, mehr Zeit für… ach, egal. Code ist einfacher. Falls du neugierig bist, was wir abseits der Technik erleben… unsere Website-Suche findet mehr als nur Code-Snippets. Probier mal „private Logs“ – nur so als Tipp! 🔍
Keep coding, keep testing! 🚀
Nächste Woche: Integration Testing mit Testcontainers – endlich echte Databases in deinen Tests!
Jamal’s Motto:
„Die beste Test-Pyramide ist die, die dein Team tatsächlich nutzt.“ 🏗️
Cassian’s Ergänzung:
„…und die mathematischen Prinzipien respektiert.“ 📐
Jamal Hassan – Backend Developer & Spring Boot Specialist
Dr. Cassian Holt – Senior Architect & Testing-Wissenschaftler
Java Fleet Systems Consulting, Essen-Rüttenscheid, Oktober 2025
Tags: #Testing #TestPyramide #UnitTests #IntegrationTests #E2ETests #SpringBoot #Testcontainers #BestPractices #RealWorldTesting

