AB 04 – Die Testpyramide
Die Situation
In der Nachbesprechung fragt die Teamleiterin:
„Ihr habt jetzt 29 Tests. Wie viele braucht ihr eigentlich? Und woran merken wir, dass es genug ist?"
Auf die zweite Frage gibt es keine Zahl als Antwort — aber es gibt eine gute Antwort.
Lernziele
Nach Bearbeitung dieses Arbeitsblatts kannst du:
- die drei Testarten einordnen und begründen, welche wofür zuständig ist
- erklären, warum es von den schnellen Tests viele und von den langsamen wenige gibt
- benennen, was ein Integrationstest zusätzlich absichert
- eine Testabdeckung messen und richtig deuten
- begründen, was man nicht testet
Was du gebaut hast
mvnw test
Gemessen am Referenzprojekt, jede Klasse einzeln gestartet:
| Testart | Was läuft | Tests | Zeit |
|---|---|---|---|
Unit — CommentPreviewTest | nur eine Klasse | 7 | 0,5 s |
Unit mit Doppel — GuestbookEntryServiceTest | eine Klasse + Mockito | 8 | 2,0 s |
Slice — GuestbookEntryControllerTest | Web-Teil von Spring | 7 | 7,4 s |
Slice — GuestbookEntryRepositoryTest | JPA-Teil + Datenbank | 6 | 9,4 s |
Integration — GaestebuchApplicationTests | die ganze Anwendung | 1 | 12,3 s |
In Lehrbüchern steht, Slice-Tests seien „um ein Vielfaches schneller" als vollständige Tests. Bei diesem Gästebuch stimmt das nur zum Teil — die Anwendung ist zu klein, als dass es sich lohnen würde.
Deutlich ist der Abstand nur ganz unten: 0,5 Sekunden gegen 12,3. Das ist der Faktor 25.
Der Abstand wächst mit der Anwendung. Ein Betriebssystem für ein mittleres Unternehmen startet nicht in 12 Sekunden, sondern in ein bis zwei Minuten — und die Unit-Tests laufen weiterhin in Sekundenbruchteilen.
Der wichtigere Grund für Slice-Tests ist ohnehin ein anderer, und den hast du in Arbeitsblatt 02 selbst erlebt.
Antwort
Die Aussagekraft eines roten Tests.
Wird GuestbookEntryControllerTest rot, liegt es an der Web-Schicht. Wird GuestbookEntryRepositoryTest rot, an der Datenbank. Wird CommentPreviewTest rot, an einer Methode, die du in zehn Sekunden gelesen hast.
Wird dagegen ein Test rot, der alles auf einmal startet, weißt du nur: irgendwo im ganzen Ding.
Ein Test ist nicht dafür da zu melden, dass etwas kaputt ist. Er ist dafür da zu zeigen, was.
Die Testpyramide
Die Form hat einen Grund. Von unten nach oben wird jede Stufe langsamer, teurer im Unterhalt und ungenauer in ihrer Aussage — dafür prüft sie mehr Zusammenspiel.
| Frage | Welcher Test? |
|---|---|
| Wird ein Kommentar mit 61 Zeichen gekürzt? | |
| Wird ein leerer Titel abgelehnt? | |
| Wird aus der Ausnahme eine 400? | |
| Zeigt Location auf den neuen Eintrag? | |
| Erzeugt die abgeleitete Abfrage das richtige SQL? | |
| Gehört Mitternacht am 1. Januar zu genau einem Jahr? | |
| Startet die Anwendung überhaupt? |
So weit unten wie möglich, so weit oben wie nötig.
Lässt sich eine Frage mit einem Unit-Test beantworten, gehört sie dorthin. Erst wenn die Frage das Zusammenspiel mehrerer Teile betrifft, steigt man eine Stufe höher.
Was der Integrationstest absichert
In deinem Projekt liegt seit Tutorial 2 eine Testklasse, die du nie beachtet hast:
@SpringBootTest
class GaestebuchApplicationTests {
@Test
void contextLoads() {
}
}
Der Test hat keinen Rumpf. Trotzdem ist er nicht sinnlos.
Was passiert
Genau ein Test wird rot: GaestebuchApplicationTests — der ohne Rumpf. Alle anderen 28 bleiben grün, auch die beiden Slice-Tests.
Denn @SpringBootTest startet die vollständige Anwendung: Alle Beans werden gebaut und miteinander verbunden. Ohne @Service kennt Spring den GuestbookEntryService nicht, kann den Controller nicht versorgen — und der Start scheitert. contextLoads wird rot, ohne je ausgeführt worden zu sein.
Warum bemerken die anderen es nicht?
- Die Unit-Tests bauen den Service selbst mit
new. Spring ist gar nicht beteiligt,@Serviceist ihnen egal. - Der
@WebMvcTestersetzt den Service ohnehin durch@MockitoBean. Ob der echte je gefunden würde, spielt keine Rolle. - Der
@DataJpaTestlädt den Service nicht — er ist keine Datenbankschicht.
Das ist genau die Klasse von Fehlern, die alle anderen Stufen durchlassen: Es passt einzeln, aber nicht zusammen. Eine vergessene Annotation, zwei Beans desselben Typs, eine fehlerhafte application.properties, eine Kreisabhängigkeit.
Deshalb steht ganz oben in der Pyramide etwas — nur eben wenig.
Nimm die Änderung zurück.
contextLoads prüft nur den Start. Ein vollwertiger Integrationstest schickt echte Anfragen durch die komplette Anwendung: Web-Schicht, Service, Repository, Datenbank — ohne ein einziges Doppel.
Er ist der Einzige, der Fehler zwischen den Schichten findet: Der Controller reicht den Parameter in der falschen Reihenfolge weiter. Der Service ruft die richtige Methode mit dem falschen Datum auf. Jede Schicht für sich ist getestet und korrekt — die Übergabe ist es nicht.
Solche Tests baust du im nächsten Tutorial.
Testabdeckung
Die Abdeckung (englisch coverage) misst, welcher Anteil deiner Zeilen beim Testlauf ausgeführt wurde.
Abdeckung misst, welche Zeilen ausgeführt wurden. Nicht, welche geprüft wurden.
Dieser Test erreicht 100 % Abdeckung für shorten:
@Test
void test() {
CommentPreview.shorten("irgendwas"); // ❌ prüft nichts
}
Jede Zeile lief. Kein einziges Assert. Der Test bleibt grün, egal was die Methode zurückgibt — er wäre selbst dann grün, wenn shorten immer null lieferte.
Umgekehrt gilt: Auch getTitle() und setTitle() erscheinen in der Statistik als abgedeckt, sobald irgendein Test irgendwo einen Titel setzt.
Abdeckung ist ein Warnsignal, keine Note. Sie beantwortet zuverlässig eine einzige Frage: Welche Stellen hat noch nie ein Test berührt? Was dort steht, ist ungeprüft — und das ist eine nützliche Information. Der Umkehrschluss gilt nicht.
Ein möglicher Satz
Weil sich die Zahl leichter durch Tests ohne Assert erhöhen lässt als durch Tests, die tatsächlich etwas prüfen — und wer unter Zeitdruck eine Zahl erreichen muss, wählt den leichteren Weg.
Was man nicht testet
| Soll das getestet werden? | ja / nein |
|---|---|
Die von Lombok erzeugten getTitle() und setTitle() | |
Dass repository.save() wirklich in die Datenbank schreibt | |
| Die Längengrenze des Titels bei 80 Zeichen | |
| Die abgeleitete Abfrage für den Jahresfilter | |
Dass Jackson aus einem String JSON macht | |
| Dass der Controller den 201-Statuscode liefert |
Teste deine eigenen Entscheidungen. Nicht die Werkzeuge, die du benutzt.
Dass Hibernate speichern kann, haben andere geprüft — tausendfach. Dass dein Methodenname die richtige Abfrage erzeugt, hat noch niemand geprüft. Genau dort liegt dein Test.
Bei abgeleiteten Abfragen ist das besonders leicht zu verwechseln. Der Test prüft nicht Spring Data, sondern deinen Namen: ob GreaterThanEqual und LessThan das ausdrücken, was du fachlich meintest.
Aufgabe 5 – Testfälle
| ID | Beschreibung | Vorbedingung | Testschritte | Erwartetes Ergebnis | Ergebnis |
|---|---|---|---|---|---|
| TF-01 | Der gesamte Testbestand läuft durch | Alle Arbeitsblätter bearbeitet |
| BUILD SUCCESS, rund 29 Tests, keine Fehler. | |
| TF-02 | Ein Fehler in der Fachlichkeit trifft genau einen Test | Alle Tests grün |
| Rot wird nur der Grenzfall 60 in CommentPreviewTest. Alle anderen Klassen bleiben grün. | |
| TF-03 | Ein Verdrahtungsfehler trifft den Integrationstest | Alle Tests grün |
| Rot wird nur GaestebuchApplicationTests — die Anwendung startet nicht mehr. Alle 28 übrigen bleiben grün, auch die beiden Slice-Tests. | |
| TF-04 | Abdeckung ohne Assert | Coverage-Lauf möglich |
| Die Abdeckung für die Klasse steigt, obwohl der Test nichts absichert. | |
| TF-05 | Die Tests sind wiederholbar | Alle Tests grün |
| Dreimal dasselbe Ergebnis. Kein Test wird mal grün, mal rot. |
Ein Test, der manchmal grün und manchmal rot ist, heißt flaky. Er ist schlimmer als kein Test: Nach dem dritten Fehlalarm schaut niemand mehr hin — und beim vierten Mal war es echt.
Typische Ursachen: Abhängigkeit von der Uhrzeit, von der Reihenfolge, von übrig gebliebenen Daten. Gegen alle drei hast du in diesem Tutorial etwas getan: das Zeitfenster statt des Zeitpunkts, das Zurückrollen nach jedem Test, die eigene Datenbank im Hauptspeicher.
Ausblick
Was noch fehlt, ist die Spitze der Pyramide: ein Test, der eine echte Anfrage durch alle Schichten schickt — ohne ein einziges Doppel, mit einer echten Datenbank dahinter.
Das ist das Thema des nächsten Tutorials, und es bekommt einen eigenen Anwendungsfall: einen Webshop mit Bestellungen, bei denen mehrere Tabellen zusammenspielen. Dort lohnt sich der Aufwand, weil es dann etwas zwischen den Schichten gibt, das schiefgehen kann.
Zusammenfassung
- Die Testpyramide: viele schnelle Unit-Tests, einige Slice-Tests, wenige Integrationstests.
- Faustregel: so weit unten wie möglich, so weit oben wie nötig.
- Der Hauptgewinn eines Slice-Tests ist nicht die Geschwindigkeit, sondern dass ein roter Test dir sagt, welche Schicht betroffen ist.
@SpringBootTestfindet Fehler, die keine andere Stufe findet: Es passt einzeln, aber nicht zusammen.- Abdeckung misst ausgeführte Zeilen, nicht geprüfte. Sie zeigt zuverlässig nur, was noch nie berührt wurde.
- Getestet werden die eigenen Entscheidungen — nicht die Werkzeuge, die man benutzt.
- Ein flaky Test ist schädlicher als kein Test.
Selbstkontrolle
- Warum gibt es von den Unit-Tests viele und von den Integrationstests wenige?
- Nenne den Hauptgrund für Slice-Tests, der nichts mit Geschwindigkeit zu tun hat.
- Welche Art von Fehler findet nur
@SpringBootTest? - Warum ist eine Abdeckung von 100 % kein Beleg für Fehlerfreiheit?
- Warum testet man die von Lombok erzeugten Getter nicht?
- Was ist ein flaky Test, und warum ist er schlimmer als gar kein Test?
Antworten
- Weil Unit-Tests schnell, billig im Unterhalt und genau in ihrer Aussage sind — von ihnen kann man viele haben und sie ständig laufen lassen. Integrationstests sind langsam und melden nur, dass irgendwo etwas klemmt; von ihnen nimmt man so wenige wie möglich.
- Die Aussagekraft: Ein roter Slice-Test benennt die betroffene Schicht. Bei einem Test, der alles zugleich startet, weiß man nur, dass irgendetwas nicht stimmt.
- Fehler im Zusammenspiel — eine fehlende Bean, eine doppelte Bean, eine Kreisabhängigkeit, eine fehlerhafte Konfiguration. Jede Schicht für sich ist in Ordnung, die Anwendung startet trotzdem nicht.
- Weil Abdeckung nur zählt, welche Zeilen ausgeführt wurden. Ein Test ohne Assert führt Zeilen aus und prüft nichts — die Zahl steigt, die Sicherheit nicht.
- Weil sie nicht dein Code sind. Lombok erzeugt sie nach einem festen Muster, das tausendfach geprüft ist. Ein Test dafür sichert nichts ab und muss trotzdem gepflegt werden.
- Ein Test, der bei unverändertem Code mal grün und mal rot ist. Er ist schädlich, weil er das Vertrauen in den gesamten Testbestand zerstört: Nach ein paar Fehlalarmen ignoriert man rote Tests — auch die echten.
Musterlösung
Hier steht der vollständige Quelltext, so wie er nach allen vier Arbeitsblättern aussieht — einschließlich der Tests. Zuerst die Anwendung mit Service-Schicht und Fehlerbehandlung, danach die vier Testklassen: Unit-Tests, der Web-Slice und der Datenbank-Slice.
Eine Lösung zu lesen fühlt sich an wie Verstehen, ist aber keines. Wer sie aufschlägt, bevor er selbst gescheitert ist, nimmt sich genau den Teil weg, an dem man etwas lernt.
Sinnvoll ist sie in zwei Fällen: nach der eigenen Lösung zum Vergleichen — oder wenn du an einer Stelle wirklich nicht weiterkommst. Dann sieh dir nur diese eine Datei an und arbeite danach selbst weiter.