Letzte Woche saß ich bei einem Maschinenbauer, 180 Mitarbeitende, ERP-Anbindung selbst geschrieben. Der Entwicklungsleiter zeigte mir stolz: 87 % Unit-Test-Coverage. Ich fragte, ob die Auftragsbestätigung in die Produktion übergeht, wenn der Lagerbestand null ist. Er wurde blass. Ein kurzer Blick in den Code: Der Handler ruft SaveChangesAsync nicht auf, wenn ein bestimmtes Feature-Flag aktiv ist. Grüne Tests, kaputtes System. Das kenne ich.
Das ist kein Einzelfall. Ich sehe es in fast jedem Mandat, wo Entwickler die Pyramide wie ein Glaubensbekenntnis behandeln: viele kleine Unit-Tests, wenig Integration, kaum End-to-End. Coverage-Zahlen hoch, Vertrauen in die Software niedrig.
Was Milan sagt
Milan Jovanović — bekannt durch seinen Newsletter zur .NET-Architektur — hat das klar ausgesprochen: Die Test-Pyramide ist veraltet. Sie entstand 2009, als Integrationstests eine geteilte Datenbank und 20-Minuten-Builds bedeuteten. Heute dreht Testcontainers in Sekunden eine frische PostgreSQL-Instanz hoch. Das Argument "echte Dependencies sind zu teuer" gilt nicht mehr.
Sein Alternativmodell: wenige Unit-Tests (15–25 %) nur für reine Domänenlogik ohne I/O, ein dicker Bauch aus Integrationstests (60–70 %) gegen echte Infrastruktur, ein schmaler Deckel aus End-to-End-Tests (<10 %) für kritische Flows — Bezahlung, Registrierung, Passwort-Reset. Dazu Layer-0-Tests für Architektur- und Contract-Grenzen. Die Form nennt er "Testing Trophy", angelehnt an Kent C. Dodds.
Sein konkretes Beispiel: Ein Handler mit 94 % Coverage hatte einen fehlenden SaveChangesAsync-Aufruf. Mock-Tests prüften das nicht, weil niemand daran dachte. Ein echter Datenbanktest hätte es sofort gezeigt.
Wie das im KMU landet
Milans Diagnose ist richtig. Seine Lösung ist es auch — für sein Umfeld. Aber sein Umfeld ist nicht Deins, wenn Du mit einem 3-Mann-Entwicklerteam in einem Mittelstandsbetrieb arbeitest.
Was sofort funktioniert: Der Kerngedanke stimmt. Integrationstests gegen echte Datenbanken sind günstiger geworden und fangen echte Bugs. Testcontainers läuft inzwischen stabil auf jedem CI-System, das Docker kann — und die meisten modernen Pipelines können das. Der Umstieg von "Mock alles" zu "teste gegen Postgres" kostet eine Woche Aufwand und gibt Dir dafür sechs Monate Sicherheit zurück. Das rechnet sich.
Was nicht direkt passt: Milan schreibt über Modular Monoliths mit Aspire Test Host, Modulkommunikation über Public APIs, Contract-Tests zwischen Services, Architektur-Enforcement über ArchUnit-ähnliche Bibliotheken. Das ist solides Engineering — für Teams, die das täglich leben. Im Mittelstand treffe ich meistens eines von zwei Szenarien: Entweder ein gewachsener Monolith ohne klare Schichten, bei dem Architektur-Tests erst mal die Architektur voraussetzen würden, die noch nicht da ist. Oder ein ausgelagertes Projekt, das von einer Agentur übergeben wurde, mit halbfertigen Tests, die niemand mehr versteht.
Was im Artikel fehlt: Der menschliche Faktor. Milan kann voraussetzen, dass sein Team Testcontainers versteht, Docker im CI konfigurieren kann und den Unterschied zwischen einem Integrations- und einem E2E-Test kennt. Das sind keine trivialen Annahmen. In einem 30-Personen-Betrieb mit zwei internen Entwicklern — von denen einer eigentlich Systemadministrator ist — ist das eine andere Realität. Ich habe erlebt, wie gut gemeinte Test-Frameworks drei Wochen Einarbeitungszeit fraßen und danach niemand mehr anfasste, weil der Alltag übernahm.
Das ist keine Kritik an Milan. Er schreibt für sein Publikum. Es ist ein Hinweis, dass Du eine Übersetzungsleistung brauchst, bevor Du seinen Ansatz adaptierst.
Konkret: Was ich empfehle
Wenn Du heute mit Testen anfangen oder neu aufstellen willst, dann so:
Schritt 1 — Einen echten Integrationstest schreiben, bevor Du umbaust. Such Dir den kritischsten Endpunkt Deiner Anwendung. Nicht den komplexesten — den kritischsten. Der, bei dem ein Fehler Kunden oder Geld kostet. Schreib einen Test, der HTTP rein, Datenbank raus macht. Kein Mock. Testcontainers oder eine lokale Testdatenbank — egal, Hauptsache echt. Dieser eine Test zeigt Dir sofort, wo Dein Setup steht und was fehlt.
Schritt 2 — Unit-Tests nicht wegwerfen, aber begrenzen. Milans Faustregel stimmt: Unit-Tests für Domänenlogik, die wirklich rein ist. Preisberechnungen, Statusübergänge, Parser. Nicht für Handler, die nur Daten durch Schichten schieben. Wenn Du Dich beim Schreiben eines Unit-Tests ertappst, wie Du drei Mocks aufbaust, um einen vierten zu testen — dann ist das ein Integrations-Test, der sich versteckt.
Schritt 3 — Zwei bis drei E2E-Tests für die existenzbedrohenden Flows. Was passiert, wenn ein Auftrag durchgeht ohne Zahlung? Was passiert, wenn ein Nutzer gelöscht wird? Diese Tests dürfen langsam sein, dürfen gelegentlich flaky sein. Sie existieren, damit Du nachts schläfst.
Was Du weglassen kannst: Architecture Tests und Contract Tests sind wertvoll — aber erst wenn Du eine Architektur hast, die es wert ist, durchzusetzen. Leg die beiseite, bis Dein Integrations-Test-Fundament steht. Prioritäten.
Zahlen, die ich in der Praxis sehe: Ein Mittelstands-Projekt mit 15–20 gut gewählten Integrationstests und 10 Unit-Tests für kritische Domänenlogik fährt stabiler als dasselbe Projekt mit 200 Unit-Tests und null Integrationstests. Das ist keine Theorie, das ist meine Erfahrung aus Mandaten in Produktion, Logistik und Dienstleistung.
Ein letzter Punkt zur Ehrlichkeit: Milans Beispiel — der fehlende SaveChangesAsync — ist kein Framework-Problem. Es ist ein Vergessen-Problem. Integration Tests lösen das nicht durch Magie, sondern weil sie die Invarianten für Dich prüfen, an die Du nicht mehr denken musst. Der Wert liegt in der Entlastung des Gedächtnisses, nicht in der Technologie.
Der vollständige Original-Beitrag von Milan Jovanović: The Test Pyramid Is a Lie (and What I Do Instead)
Wenn Du wissen willst, wie Dein konkretes Projekt dasteht und wo der erste Integrations-Test am meisten bringt: → 30 Min Klartext-Sparring
— Bernhard