Vom Cypress/Playwright-Wildwuchs zurück zur gesunden Testpyramide
Viele Teams verlassen sich fast nur auf langsame End to End Tests und manuelle Klicktests, weil eine Unit Test Kultur nie entstanden ist. Dieses Ice Cream Cone Anti Pattern kehrt die klassische Testpyramide um und führt zu trägen CI Pipelines, hoher Flakiness und später Fehlererkennung. Dieser Artikel zeigt, wie Teams das Muster erkennen und mit einem praktischen Migrationspfad zu einer schnellen, verlässlichen Testpyramide zurückfinden.
Inhaltsverzeichnis
- 1. Was ist das Ice-Cream-Cone-Anti-Pattern
- 2. Symptome einer invertierten Testpyramide
- 3. Warum Teams dort landen: fehlende Unit-Test-Kultur
- 4. Warum E2E-Tests "realer" wirken als Unit-Tests
- 5. Die Kosten des Anti-Patterns in Zahlen
- 6. Das eigene Test-Portfolio diagnostizieren
- 7. Ein praktischer Migrationspfad zurück zur Pyramide
- 8. Kulturwandel im Team verankern
- 9. Anti-Pattern und gesunde Pyramide im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was ist das Ice-Cream-Cone-Anti-Pattern
Das Ice-Cream-Cone-Anti-Pattern beschreibt eine Testarchitektur, in der sich das Verhältnis der klassischen Testpyramide nach Mike Cohn umkehrt: Statt vieler schneller Unit-Tests an der Basis und weniger, gezielter E2E-Tests an der Spitze, stapeln sich hunderte langsame Cypress- oder Playwright-Specs und manuelle Klicktests oben, während unten kaum Unit-Tests existieren. Gemalt als Diagramm ergibt das die Form einer Eistüte statt einer Pyramide, daher der Name.
In der Praxis sieht das konkret so aus: Ein Team testet eine Rabattberechnung nicht mit einer einzigen Funktion und drei Assertions, sondern öffnet einen Browser, loggt sich ein, füllt einen Warenkorb, klickt durch den Checkout und prüft am Ende einen Text im DOM. Die QA-Abteilung klickt vor jedem Release manuell die immer gleiche Checkliste durch. Die eigentliche Geschäftslogik ist nur indirekt, über fünf UI-Schritte hinweg, überhaupt getestet.
2. Symptome einer invertierten Testpyramide
Das auffälligste Symptom ist die CI-Laufzeit: Pipelines, die 45 bis 90 Minuten brauchen, weil hunderte Browser-Sessions sequenziell oder nur teilweise parallelisiert durchlaufen. Dazu kommen Flaky Tests, die nicht wegen echter Bugs rot werden, sondern wegen Timing-Problemen, Netzwerklatenz oder Race Conditions im UI. Retterversuche wie automatische Re-Runs verschleiern das Problem, statt es zu lösen, und die Pipeline wird insgesamt noch langsamer.
Genauso verräterisch ist das Verhalten im Team: Entwickler trauen sich nicht, Code zu refactoren, ohne vorher die komplette E2E-Suite lokal laufen zu lassen, weil das die einzige Quelle von Vertrauen ist. Pull-Request-Reviews fragen "ist die E2E-Pipeline grün?" statt die eigentliche Logik zu prüfen. Bugs in reiner Berechnungslogik, die ein Unit-Test in Millisekunden gefunden hätte, tauchen stattdessen erst in Produktion auf.
// cypress/e2e/checkout/discount-calculation.cy.js
// PROBLEM: the only place discount math is verified is a full browser E2E run
describe('Checkout discount calculation', () => {
it('applies a 10% loyalty discount to the cart total', () => {
cy.login('loyalty-customer@example.com', 'password123');
cy.visit('/cart');
cy.get('[data-testid="cart-item"]').should('have.length', 3);
cy.get('[data-testid="apply-loyalty-discount"]').click();
// The actual assertion we care about is a pure calculation,
// but we can only reach it through five UI steps and a full page load.
cy.get('[data-testid="cart-total"]')
.invoke('text')
.then((text) => {
const total = parseFloat(text.replace('€', '').trim());
expect(total).to.equal(179.91); // 199.90 * 0.9, rounded
});
});
});
3. Warum Teams dort landen: fehlende Unit-Test-Kultur
Der häufigste Grund ist historisch: Ein Codebestand ist über Jahre gewachsen, ohne dass Dependency Injection, klare Modulgrenzen oder testbare Funktionen etabliert wurden. Geschäftslogik ist eng an DOM-Zugriffe, globale Zustände oder API-Aufrufe gekoppelt, sodass ein isolierter Unit-Test technisch aufwendig wird. "Wir schreiben die Tests später nach" ist in solchen Codebasen ein Versprechen, das praktisch nie eingelöst wird, weil jedes neue Feature Priorität vor Nacharbeit bekommt.
Dazu kommen organisatorische Anreize: Das QA-Team wird an der Anzahl gefundener Bugs vor Release gemessen, nicht an der Entwicklergeschwindigkeit, was manuelle und E2E-Tests strukturell bevorzugt. Führungskräfte sehen einen grünen Haken in der E2E-Pipeline als Qualitätsbeweis, ohne die tatsächliche Testabdeckung der Logik zu hinterfragen. Code-Reviews verlangen selten explizit "Wo ist der Unit-Test für diese Funktion?", wodurch sich die Lücke mit jedem Merge vergrößert.
4. Warum E2E-Tests "realer" wirken als Unit-Tests
E2E-Tests fühlen sich intuitiv vertrauenswürdiger an, weil sie exakt den Weg gehen, den auch ein echter Nutzer nimmt: Browser öffnen, klicken, tippen, Ergebnis sehen. Diese psychologische Nähe zur Realität erzeugt ein falsches Sicherheitsgefühl nach dem Motto "wenn die E2E-Suite grün ist, funktioniert alles", obwohl E2E-Tests notorisch lückenhaft in Edge Cases sind, weil jeder zusätzliche Fall einen weiteren kompletten Durchlauf kostet.
Unit-Tests wirken dagegen abstrakter: Sie verlangen, Abhängigkeiten zu isolieren, Mocks zu bauen und eine Funktion losgelöst vom Gesamtsystem zu denken. Für schlecht strukturierten Code ist das schwieriger als ein Klick-Recorder wie Cypress Studio, der ohne Testdesign sofort ein lauffähiges Skript liefert. Entwickler ohne TDD-Erfahrung empfinden E2E-Tests deshalb als den einfacheren Einstieg, obwohl sie langfristig die teurere und langsamere Absicherung sind.
// src/pricing/discount.js
// Business logic extracted into a pure, framework-free function
export function applyLoyaltyDiscount(cartTotal, discountRate = 0.1) {
if (cartTotal < 0) {
throw new Error('Cart total cannot be negative');
}
return Math.round(cartTotal * (1 - discountRate) * 100) / 100;
}
// src/pricing/discount.test.js
// Vitest/Jest unit test: runs in milliseconds, no browser, no network
import { describe, it, expect } from 'vitest';
import { applyLoyaltyDiscount } from './discount.js';
describe('applyLoyaltyDiscount', () => {
it('applies the default 10% discount and rounds to two decimals', () => {
expect(applyLoyaltyDiscount(199.90)).toBe(179.91);
});
it('supports a custom discount rate', () => {
expect(applyLoyaltyDiscount(100, 0.25)).toBe(75);
});
it('rejects a negative cart total', () => {
expect(() => applyLoyaltyDiscount(-5)).toThrow('Cart total cannot be negative');
});
});
5. Die Kosten des Anti-Patterns in Zahlen
Die direkten Kosten sind messbar: CI-Minuten, die bei Cloud-Runnern abgerechnet werden, Wartezeit von Entwicklern zwischen Commit und Feedback, und Kontextwechsel, weil ein 60-Minuten-Lauf niemanden am Bildschirm hält. Wo ein Unit-Test in Sekunden ein Ergebnis liefert, wartet ein Team bei einer E2E-lastigen Pipeline oft Stunden auf die erste verlässliche Rückmeldung, was Release-Zyklen künstlich verlängert.
Die indirekten Kosten wiegen schwerer: Flaky Tests untergraben das Vertrauen in die Pipeline, bis Teams rote Builds reflexhaft neu starten, statt sie zu untersuchen. Echte Regressionen werden dann als "wahrscheinlich nur wieder flaky" abgetan und übersehen. Ohne ein schnelles Unit-Test-Netz als erste Verteidigungslinie wiederholen sich dieselben Bugklassen, weil niemand eine gezielte, dauerhafte Regressionsabsicherung für die eigentliche Logik gebaut hat.
# .github/workflows/test-pyramid-health.yml
# Tracks runtime and count per test tier so pyramid health is visible over time
name: Test Pyramid Health
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- name: Run unit tests with timing
run: npm run test:unit -- --reporter=json --outputFile=reports/unit.json
- name: Report unit-tier metrics
run: node scripts/report-tier-metrics.js unit reports/unit.json
integration:
runs-on: ubuntu-latest
needs: unit
steps:
- uses: actions/checkout@v4
- run: npm ci
- name: Run integration tests with timing
run: npm run test:integration -- --reporter=json --outputFile=reports/integration.json
- name: Report integration-tier metrics
run: node scripts/report-tier-metrics.js integration reports/integration.json
e2e:
runs-on: ubuntu-latest
needs: integration
steps:
- uses: actions/checkout@v4
- run: npm ci
- name: Run Cypress E2E suite with timing
run: npx cypress run --reporter json --reporter-options "output=reports/e2e.json"
- name: Report e2e-tier metrics
run: node scripts/report-tier-metrics.js e2e reports/e2e.json
publish-pyramid-report:
runs-on: ubuntu-latest
needs: [unit, integration, e2e]
steps:
- name: Combine tier metrics into pyramid ratio
run: node scripts/publish-pyramid-ratio.js
6. Das eigene Test-Portfolio diagnostizieren
Bevor migriert wird, muss der Ist-Zustand messbar sein: Wie viele Tests existieren pro Ebene, wie lange läuft jede Ebene, und welches Verhältnis ergibt sich daraus? Ein einfacher Zähl-Skript über die Testverzeichnisse liefert bereits die Grundzahlen. Entscheidend ist dabei, nicht nur zu zählen, sondern zu klassifizieren, was ein Test tatsächlich prüft, denn viele E2E-Specs verifizieren in Wahrheit reine Berechnungslogik, die auch ohne Browser testbar wäre.
Ein Audit-Durchlauf markiert genau diese Kandidaten: Specs, die auf konkrete berechnete Werte prüfen, sind fast immer Kandidaten für einen begleitenden oder ersetzenden Unit-Test. Specs, die reines UI-Verhalten wie Sichtbarkeit oder Navigation prüfen, gehören dagegen zu Recht auf die E2E-Ebene. Diese Klassifizierung liefert die Priorisierungsliste für die eigentliche Migration.
#!/usr/bin/env bash
# audit-cypress-suite.sh - classify existing Cypress specs by what they actually verify
set -euo pipefail
SPEC_DIR="cypress/e2e"
declare -A tier_count=([ui-behavior]=0 [business-logic]=0 [smoke]=0 [unclear]=0)
for spec in $(find "$SPEC_DIR" -name "*.cy.js"); do
# Heuristic: specs asserting on computed numbers often hide business logic
if grep -qE "expect\(.*\)\.to\.(equal|eq)\([0-9]" "$spec"; then
tier_count[business-logic]=$((tier_count[business-logic] + 1))
echo "[CANDIDATE FOR UNIT TEST] $spec"
elif grep -qE "cy\.get\(.*\)\.should\('be.visible'\)" "$spec"; then
tier_count[ui-behavior]=$((tier_count[ui-behavior] + 1))
elif grep -qE "@smoke" "$spec"; then
tier_count[smoke]=$((tier_count[smoke] + 1))
else
tier_count[unclear]=$((tier_count[unclear] + 1))
echo "[NEEDS REVIEW] $spec"
fi
done
echo "--- Cypress suite audit summary ---"
for tier in "${!tier_count[@]}"; do
echo "$tier: ${tier_count[$tier]}"
done
7. Ein praktischer Migrationspfad zurück zur Pyramide
Der bewährte Ansatz ist die Strangler-Fig-Migration statt eines großen Rewrites: Ab sofort verlangt jeder Pull Request mit neuer Geschäftslogik einen begleitenden Unit-Test, ohne Ausnahme. Parallel identifiziert eine Git-Churn-Analyse die am häufigsten geänderten oder fehleranfälligsten Module im Legacy-Code, denn dort lohnt sich das Nachrüsten von Unit-Tests am meisten pro investierter Stunde.
Der zweite, oft vernachlässigte Schritt: Die E2E-Suite aktiv verkleinern. Sobald ein Unit- oder Integrationstest dieselbe Logik zuverlässig abdeckt, wird der redundante E2E-Test gelöscht oder zu einem reinen Smoke-Test heruntergestuft, der nur noch prüft, ob der kritische Pfad überhaupt lädt. Getrennte npm-Skripte pro Ebene machen diese Struktur sichtbar und erzwingbar, sowohl lokal als auch in der CI-Pipeline.
{
"scripts": {
"test:unit": "vitest run --dir src",
"test:unit:watch": "vitest --dir src",
"test:integration": "vitest run --dir tests/integration --pool=forks",
"test:e2e:smoke": "cypress run --env grepTags=@smoke",
"test:e2e:full": "cypress run",
"test:ci": "npm run test:unit && npm run test:integration && npm run test:e2e:smoke",
"test:pyramid-ratio": "node scripts/publish-pyramid-ratio.js"
}
}
8. Kulturwandel im Team verankern
Technische Migration allein hält nicht, wenn sich die Teamkultur nicht mitverändert. Die Definition of Done sollte explizit "Unit-Test für neue Logik vorhanden" enthalten, und Code-Reviews sollten diese Frage aktiv stellen, statt sie zu übergehen. Pairing- oder Mob-Programming-Sessions, in denen erfahrene Entwickler Mocking-Patterns und Testdesign an Kollegen weitergeben, senken die Einstiegshürde spürbar schneller als reine Dokumentation.
Sichtbarkeit hält den Wandel am Leben: Ein Dashboard, das das Verhältnis von Unit- zu E2E-Tests und die CI-Laufzeit pro Ebene über Zeit zeigt, macht Fortschritt greifbar und wird zum wiederkehrenden Thema in Retrospektiven. Wenn eine sinkende E2E-Laufzeit als Teamerfolg gefeiert wird statt als Nebensache zu gelten, wird aus der Migration ein gemeinsames Ziel statt der Initiative einer einzelnen Person.
9. Anti-Pattern und gesunde Pyramide im Vergleich
Die folgende Übersicht stellt die typischen Kennzahlen eines Ice-Cream-Cone-Anti-Patterns den Werten einer gesunden Testpyramide gegenüber - als konkrete Zielmarken für die eigene Migration.
| Kriterium | Ice-Cream-Cone (Anti-Pattern) | Gesunde Pyramide | Praktischer Hebel |
|---|---|---|---|
| Testverteilung | 70% E2E, 20% Integration, 10% Unit | 70% Unit, 20% Integration, 10% E2E | Testarten nach Risiko und Geschwindigkeit gewichten |
| CI-Laufzeit | 45-90 Minuten pro Lauf | 5-12 Minuten pro Lauf | Unit-Tests laufen parallel in Millisekunden |
| Flakiness-Rate | 15-30% instabile Läufe | unter 2% instabile Läufe | Weniger Netzwerk- und Timing-Abhängigkeiten |
| Feedback-Geschwindigkeit | Stunden bis zum Ergebnis | Sekunden bis Minuten | Fehler vor dem Commit statt nach dem Deploy |
| Onboarding neuer Entwickler | Wochen, um die Cypress-Suite zu verstehen | Tage, da Unit-Tests Logik dokumentieren | Tests als lebende Dokumentation nutzen |
In der Praxis verstärken sich die Kennzahlen gegenseitig: Eine hohe Flakiness-Rate verlangsamt nicht nur die CI-Laufzeit, sondern zerstört auch das Vertrauen ins Onboarding neuer Entwickler, die die instabile Suite als Normalzustand kennenlernen. Wer konsequent an der Testverteilung arbeitet, verbessert automatisch auch Laufzeit, Stabilität und Einarbeitungszeit gemeinsam statt isoliert.
Mironsoft
Teststrategie, Testpyramiden-Migration und CI/CD-Tooling für Entwicklerteams
Testportfolio wieder auf gesunde Beine stellen?
Wir analysieren eure bestehende Cypress- oder Playwright-Suite, klassifizieren jeden Test nach dem, was er tatsächlich prüft, und begleiten euch bei der schrittweisen Migration zurück zu einer schnellen, verlässlichen Testpyramide.
Testportfolio-Diagnose
Audit der bestehenden E2E-Suite und Klassifizierung nach Testebene
Migrationsplan zur Pyramide
Priorisierte Roadmap nach Git-Churn und Risiko, Strangler-Fig-Ansatz
Unit-Test-Kultur aufbauen
Pairing-Sessions, Definition of Done und CI-Reporting pro Testebene
10. Zusammenfassung
Das Ice-Cream-Cone-Anti-Pattern entsteht selten aus einer bewussten Entscheidung, sondern schleichend: fehlende Unit-Test-Kultur, historisch gewachsener Code und die trügerische Nähe von E2E-Tests zur echten Nutzererfahrung summieren sich über Jahre zu trägen, teuren und instabilen Testsuiten. Die Symptome sind messbar - lange CI-Läufe, hohe Flakiness, spätes Bug-Feedback - und genauso messbar ist der Weg zurück.
Ein praktischer Migrationspfad kombiniert eine ehrliche Diagnose des bestehenden Portfolios, den Strangler-Fig-Ansatz für neuen und kritischen Legacy-Code, das gezielte Verkleinern der E2E-Suite und einen Kulturwandel, der Unit-Tests zum selbstverständlichen Teil jedes Pull Requests macht. Keiner dieser Schritte erfordert einen kompletten Rewrite - sie erfordern Konsequenz über mehrere Monate hinweg.
Das Ice-Cream-Cone-Anti-Pattern - Das Wichtigste auf einen Blick
Was das Anti-Pattern ist
Invertierte Testpyramide: viele langsame E2E- und manuelle Tests oben, kaum Unit-Tests unten - benannt nach der Eistüten-Form im Diagramm.
Diagnose
Tests pro Ebene zählen, Laufzeit messen und E2E-Specs identifizieren, die in Wahrheit nur Geschäftslogik prüfen.
Migrationspfad
Strangler-Fig-Ansatz: Unit-Tests für neuen Code verpflichtend, Legacy-Module nach Git-Churn priorisiert nachrüsten, E2E-Suite aktiv verkleinern.
Kulturwandel
Unit-Tests in Definition of Done, Pairing-Sessions für Testdesign, Pyramiden-Verhältnis sichtbar in CI-Reports und Retros.