Das Ice-Cream-Cone-Anti-Pattern erkennen und umkehren
PASS
expect()
Testing · E2E · Testpyramide · Cypress · Playwright
Das Ice-Cream-Cone-Anti-Pattern erkennen und umkehren
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.

12 Min. Lesezeit Anti-Pattern · Testpyramide · Teststrategie Cypress · Playwright · Testkultur

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.

11. FAQ: Ice-Cream-Cone-Anti-Pattern

1Was ist der Unterschied zwischen dem Ice-Cream-Cone-Anti-Pattern und der klassischen Testpyramide?
Die klassische Pyramide setzt auf viele Unit-Tests als Basis und wenige E2E-Tests an der Spitze. Das Anti-Pattern kehrt das um: viele E2E-Tests oben, kaum Unit-Tests unten.
2Woran erkenne ich, dass mein Team im Anti-Pattern gefangen ist?
CI-Läufe über 30 Minuten, hunderte Cypress/Playwright-Specs, kaum Unit-Tests, hohe Flakiness und Angst vor Refactoring ohne komplette E2E-Suite.
3Muss ich alle E2E-Tests löschen, um die Pyramide zu reparieren?
Nein. Kritische User Journeys bleiben E2E-getestet. Nur redundante E2E-Tests für reine Geschäftslogik werden durch Unit-Tests ersetzt.
4Wie fange ich an, wenn wir kaum Unit-Tests haben?
Strangler-Fig-Ansatz: Unit-Tests für jeden neuen Code-Change verpflichtend machen, Legacy-Module per Git-Churn-Analyse priorisiert nachrüsten.
5Welche Tools eignen sich für die drei Testebenen?
Jest/Vitest für Unit-Tests, dieselben Runner mit echten Modulgrenzen oder Testcontainers für Integrationstests, Cypress/Playwright für kritische E2E-Journeys.
6Wie lange dauert die Migration realistisch?
Drei bis sechs Monate für ein mittelgroßes Projekt bei konsequenter Umsetzung. Vollständige Umkehrung dauert oft länger.
7Wie überzeuge ich das Management?
Mit Zahlen: CI-Laufzeit, Flakiness-Rate und Time-to-Feedback vor/nach der Migration gegenüberstellen. Kosten- und Geschwindigkeitsargumente überzeugen mehr als Codequalität allein.
8Integrationstests vs. E2E-Tests: was ist der Unterschied?
Integrationstests prüfen das Zusammenspiel mehrerer Module ohne vollständigen Browser. E2E-Tests laufen im echten Browser gegen die vollständige Anwendung.
9Wie verhindere ich Rückfall in alte Muster?
Pyramiden-Verhältnis als Metrik in CI-Reports sichtbar machen und in Retros besprechen, verankert als Teil der Definition of Done.
10Sind Snapshot-Tests Teil der Lösung oder Teil des Problems?
Beides. Gezielte Snapshot-Tests für stabile Komponenten sind sinnvoll, pauschale Snapshots ganzer Seiten verschleiern fehlende echte Assertions.