Zum Hauptinhalt springen

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:

TestartWas läuftTestsZeit
Unit — CommentPreviewTestnur eine Klasse70,5 s
Unit mit Doppel — GuestbookEntryServiceTesteine Klasse + Mockito82,0 s
Slice — GuestbookEntryControllerTestWeb-Teil von Spring77,4 s
Slice — GuestbookEntryRepositoryTestJPA-Teil + Datenbank69,4 s
Integration — GaestebuchApplicationTestsdie ganze Anwendung112,3 s
Ehrlich gesagt: die Unterschiede sind hier kleiner, als oft behauptet wird

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

wenigeeinigevieleIntegrationstests@SpringBootTestdie ganze Anwendung — langsam, teuer im Unterhaltdafür: prüft das ZusammenspielSlice-Tests@WebMvcTest · @DataJpaTestje eine Schicht — mittelein roter Test benennt die SchichtUnit-Testseine Klasse, ohne Springschnell, billig — sehr genaue Aussagelangsamer · teurer · ungenauerSo weit unten wie möglich, so weit oben wie nötig

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.

FrageWelcher 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?
0 von 7 ausgefüllt
Die Faustregel

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:

src/test/java/de/szut/gaestebuch/GaestebuchApplicationTests.java
@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, @Service ist ihnen egal.
  • Der @WebMvcTest ersetzt den Service ohnehin durch @MockitoBean. Ob der echte je gefunden würde, spielt keine Rolle.
  • Der @DataJpaTest lä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.

Was ein echter Integrationstest zusätzlich tut

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.

Die gefährlichste Zahl im Testwesen

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
0 von 6 ausgefüllt
Die Trennlinie

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

IDBeschreibungVorbedingungTestschritteErwartetes ErgebnisErgebnis
TF-01Der gesamte Testbestand läuft durchAlle Arbeitsblätter bearbeitet
  1. mvnw test
BUILD SUCCESS, rund 29 Tests, keine Fehler.
TF-02Ein Fehler in der Fachlichkeit trifft genau einen TestAlle Tests grün
  1. In CommentPreview <= wieder zu < ändern
  2. Alle Tests ausführen
  3. Zurücknehmen
Rot wird nur der Grenzfall 60 in CommentPreviewTest. Alle anderen Klassen bleiben grün.
TF-03Ein Verdrahtungsfehler trifft den IntegrationstestAlle Tests grün
  1. Im Service @Service entfernen
  2. Alle Tests ausführen
  3. Zurücknehmen
Rot wird nur GaestebuchApplicationTests — die Anwendung startet nicht mehr. Alle 28 übrigen bleiben grün, auch die beiden Slice-Tests.
TF-04Abdeckung ohne AssertCoverage-Lauf möglich
  1. Einen Test schreiben, der shorten("x") aufruft und nichts prüft
  2. Mit Coverage laufen lassen
  3. Test wieder löschen
Die Abdeckung für die Klasse steigt, obwohl der Test nichts absichert.
TF-05Die Tests sind wiederholbarAlle Tests grün
  1. mvnw test dreimal hintereinander ausführen
Dreimal dasselbe Ergebnis. Kein Test wird mal grün, mal rot.
TF-05 prüft die wichtigste Eigenschaft überhaupt

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

Das hast du gelernt
  • 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.
  • @SpringBootTest findet 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

  1. Warum gibt es von den Unit-Tests viele und von den Integrationstests wenige?
  2. Nenne den Hauptgrund für Slice-Tests, der nichts mit Geschwindigkeit zu tun hat.
  3. Welche Art von Fehler findet nur @SpringBootTest?
  4. Warum ist eine Abdeckung von 100 % kein Beleg für Fehlerfreiheit?
  5. Warum testet man die von Lombok erzeugten Getter nicht?
  6. Was ist ein flaky Test, und warum ist er schlimmer als gar kein Test?
Antworten
  1. 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.
  2. Die Aussagekraft: Ein roter Slice-Test benennt die betroffene Schicht. Bei einem Test, der alles zugleich startet, weiß man nur, dass irgendetwas nicht stimmt.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

Erst selbst versuchen

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.

Musterlösung

Das Passwort bekommst du von deiner Lehrkraft.