CodeGym /Kurse /Spring Test /Tests als System

Tests als System

Spring Test
Level 1, Lektion 2
Verfügbar

1. Wir schauen nicht auf den Namen, sondern auf zwei Achsen

In Gesprächen über Tests kann man sich schon auf Wortebene leicht verwirren. In einem Team ist ein „Integration Test" fast jeder Test mit Spring. In einem anderen – nur ein Live-Server-Test über echtes HTTP. Deshalb ist es praktischer, sich nicht auf den Dateinamen zu stützen, sondern auf zwei praktische Achsen: was im Test wirklich läuft und was mit diesem Test überhaupt bewiesen werden kann.

Die erste Achse – Umfang der realen Umgebung. Lässt du ein einzelnes Java-Objekt im Speicher laufen? Fährst du die Web-Schicht hoch? Fährst du einen vollständigen Spring-Context hoch? Verwendest du einen echten HTTP-Server? Fügst du ein echtes PostgreSQL in einem Container hinzu? Je mehr echte Welt du zum Teil des Tests machst, desto höher sind die Startkosten und desto mehr potenzielle Stellen gibt es, wo der Test aus einem falschen Grund brechen kann – also nicht wegen dem, was du gerade untersuchst.

Die zweite Achse – Beweiszone. Manche Tests fangen am besten lokale Regeln, andere – HTTP-Grenzverträge, wieder andere – Wiring zwischen Schichten, und weitere – Details der realen Umgebung. Wenn man diese zwei Achsen im Kopf behält, wird es sofort einfacher: Man wählt nicht den „trendigen" Test, sondern die angemessene Prüftiefe für ein konkretes Risiko.

2. Unit-Tests: günstigster Schutz für lokalen Sinn

Die Unit-Ebene ist der Ort, wo man ein Stück Verhalten ohne Spring, ohne Datenbank, ohne Netzwerk und ohne zufällige Infrastruktur drumherum prüft. Solch ein Test ist fast immer schnell, präzise und günstig in der Diagnose. Wenn er fehlschlägt, lässt sich die Ursache normalerweise in Sekunden verstehen, ohne sich durch die halbe Anwendung zu wühlen.

In ContentHub sind natürliche Ziele für Unit-Prüfungen Policy-Objekte, kleine Domänenregeln, Slug-Generierung, Status-Übergangs-Beschränkungen, lokale Datentransformationen. Alles, was man ehrlich als gewöhnliche Java-Logik beobachtbar machen kann. Zum Beispiel lebt die Regel „approve ist nur aus IN_REVIEW erlaubt" wunderbar auf dieser Ebene.

// Policy-Objekt: kapselt eine Domänenregel (lokale Logik ohne Spring/DB/HTTP)
class PublicationPolicy {

    // Prüfen, ob approve aus dem aktuellen Artikel-Status ausgeführt werden darf
    boolean canApprove(ArticleStatus status) {
        // Einziger erlaubter Zustand für approve – IN_REVIEW
        // Jeder andere Status (z. B. DRAFT) soll false zurückgeben
        return status == ArticleStatus.IN_REVIEW;
    }
}

Solcher Code braucht keinen ApplicationContext, kein MVC und keine echte Datenbank, um eine Regression zu erkennen. Wenn morgen jemand „zur Beschleunigung des Prozesses" approve aus DRAFT erlaubt, sollte der Unit-Test das sofort auffangen. Das ist ein starkes Argument für die Unit-Ebene: Hier fängst du den Fehler genau dort, wo er entstanden ist, und nicht nach einer langen Kette indirekter Effekte.

Aber man sollte bis zum Ende ehrlich sein. Ein Unit-Test beweist nicht, dass der HTTP-Endpoint diese Regel korrekt aufruft. Er beweist nicht, dass Daten in der Datenbank gespeichert wurden. Er beweist nicht, dass Security keinen unerwünschten Benutzer durchlässt. Er schützt den lokalen Sinn. Und genau deshalb ist er so wertvoll: Er verspricht nichts Übermäßiges.

3. Slice-Tests: echte Infrastruktur einer einzelnen Schicht

Sobald das Risiko von der Ebene „reine Regel" auf die Ebene HTTP, JSON oder Persistenz wechselt, beginnt der Unit-Ansatz entweder das Problem unterzuabdecken oder in ein Theater von Mocks auszuarten. Genau hier ist in der Spring-Boot-Ökosystem die Idee der Test-Slices besonders nützlich: Man fährt nicht die gesamte Anwendung hoch, sondern ein kontrolliertes Stück, in dem die Infrastruktur einer bestimmten Schicht real wird.

Slice-Tests haben eine wichtige Philosophie. Das ist keine „Unter-Integration" und kein Kompromiss aus Armut. Das ist ein normaler Weg, eine Schicht ehrlich und schnell zu prüfen. Für die Web-Schicht ist es wichtig, dass Routing, Parameter-Binding, Validierung, Serialisierung und Fehlerübersetzung wirklich funktionieren. Für JSON-Verträge ist das echte Verhalten des Serialisierers wichtig. Für die Datenschicht sind Abfragen, Mappings, Beschränkungen und generell das Verhalten dieser Schicht wichtig. In all diesen Fällen ist es nicht notwendig, das vollständige Backend hochzufahren – und oft sogar schädlich.

Deshalb wird die Slice-Ebene in diesem Kurs ein riesiger Teil der täglichen Praxis sein. Sie lehrt eine sehr nützliche Disziplin: genau das Stück Infrastruktur real zu machen, wo das Risiko lebt, und alles andere ohne Grund nicht mitzuschleppen. Das ist eine der professionellsten Fähigkeiten in einem normalen Spring-Boot-Projekt.

4. Integration-Tests: wenn man den Übergang prüfen muss, nicht nur die Schicht

Ein Integrationstest beginnt dort, wo der Fehler nicht innerhalb einer Schicht lebt, sondern an der Verbindung mehrerer Teile. Der Controller ruft den Service auf, der Service arbeitet mit dem Repository, daneben sind Security-Filter, Fehlerbehandlung, Konfiguration, Serialisierung angebunden – und man möchte sehen, wie sich all das zusammen verhält. Hier reicht ein ehrlicher Slice-Test nicht mehr aus, weil das interessante Risiko genau an der Verbindungsstelle entstanden ist.

In Spring-Boot-Projekten verbindet man das Wort Integration oft mit dem vollständigen Anwendungskontext. Das ist eine normale Assoziation, aber hier ist nicht der Name der Annotation wichtig, sondern der Gedanke: Man startet ein ausreichend breites Stück des Systems, um Wiring, Konfiguration, Exception-Übersetzung, die Kette Controller → Service → Repository und andere gemeinsame Effekte zu fangen.

Der Preis dieser Ebene ist auch ehrlich. Sie startet länger, fällt lauter und lokalisiert die Fehlerursache schlechter. Deshalb sind Integrationstests wertvoll nicht durch ihre Breite an sich, sondern dadurch, dass sie genau die Risiken schließen, die wirklich zwischen Systemteilen leben. Sobald man diese Bedingung vergisst, beginnt der Missbrauch des vollständigen Kontexts.

5. Live-Server: wenn echter HTTP-Austausch wichtig ist

Es gibt einen noch breiteren Schritt: ein Test, der nicht über einen lokalen Web-Schicht-Aufruf läuft, sondern über einen tatsächlich hochgefahrenen HTTP-Server. Aus der Sicht des Entwicklers ist das eine sehr wichtige Grenze. Ein Intra-Prozess-Web-Test und ein Live-Server prüfen ähnliche Szenarien, aber nicht dieselbe Realität. Im zweiten Fall verhält sich die Anwendung näher an einem echten Server, und der Test kommuniziert mit ihr wie ein externer Client.

Was fügt das hinzu? Erstens, Sicherheit über den echten HTTP-Round-Trip: Der Server ist wirklich auf einem Port hochgefahren, die Anfrage ist wirklich gesendet worden und eine Antwort zurückgekommen. Zweitens werden einige Details des Transportverhaltens, der Filter und der Server-Konfiguration besser sichtbar. Drittens zeigt dieser Modus sehr gut, dass der Client „von außen" tatsächlich das System sieht, das man verspricht, und nicht nur einen korrekten JVM-internen Aufruf.

Aber einen Live-Server als Standard-Modus für die gesamte Testsuite zu verwenden, ist nicht möglich. Er ist teuer. Wenn man eine lokale Workflow-Regel prüft, ist der Live-Server einfach eine langsame Dekoration. Seine Stärke zeigt sich nur dort, wo die Frage tatsächlich so lautet: „Was sieht ein echter HTTP-Client?"

6. Container-Tests: Verhalten hängt von der Infrastruktur ab

Container-Tests braucht man nicht, um alles „noch seriöser zu machen", sondern um aufzuhören, sich selbst über das Verhalten einer externen Komponente zu belügen. Für ContentHub ist das Hauptbeispiel hier ein echtes PostgreSQL. Auf vereinfachter Ebene können Daten normal aussehen, aber auf einer echten Datenbank tauchen plötzlich Dialekt, Constraints, Timestamps, Sortierungen und Migrationen auf, die man weder im Kopf noch durch einen zufälligen Test-Ersatz ehrlich simulieren kann.

Deshalb ist ein Container-Test nicht „die letzte und richtigste Ebene". Es ist ein separates Werkzeug für eine separate Risikokategorie: wenn einem das Verhalten einer konkreten Infrastruktur wichtig ist. In einem solchen Test zahlt man mit Startzeit und zusätzlicher Umgebungskomplexität, bekommt aber ein viel ehrlicheres Bild dort, wo die echte Umgebung das Ergebnis tatsächlich verändert.

Genau deshalb erscheint der Container-Path in einer reifen Strategie normalerweise später und punktuell. Nicht als Ersatz für alle anderen Tests, sondern als Verstärker für besonders sensible Szenarien. Das ist ein wichtiger Gedanke für den gesamten Kurs: Teurer bedeutet nicht besser. Teurer bedeutet „sollte dort eingesetzt werden, wo ein zusätzlicher Wert vorhanden ist" 💡

7. Regressionstests

Es gibt noch ein Wort, das man gerne in eine Reihe mit Unit und Integration stellt – Regression. Aber eine Regressions-Testsuite versteht man besser nicht als separate Technologie, sondern als einen speziell ausgewählten Teil der gesamten Test Suite. Ihr Sinn liegt nicht darin, die größte oder schwerste zu sein. Ihr Sinn liegt darin, maximalen Nutzen für eine angemessene Laufzeit zu liefern.

In einem normalen Backend-Projekt ist eine Regressions-Suite fast immer gemischt. Sie enthält schnelle und wertvolle Unit-Tests, ein paar wichtige Slice-Prüfungen, einige teure aber sehr wichtige Integrations-Szenarien. Manchmal gehören auch Live-Server-Prüfungen dazu, wenn der Preis gerechtfertigt ist. Container-Tests bleiben hingegen häufiger in einem späteren und engeren Teilsatz, weil sie im täglichen Betrieb wesentlich teurer sind.

Das ist jetzt nützlich zu verstehen: eine gute Suite ist kein Haufen gleich breiter Tests. Es ist eine Komposition verschiedener Ebenen, wo jede für sich mit einem klaren Nutzen bezahlt. Und die Regressions-Logik hilft genau dabei, diese Komposition in einem vernünftigen Zustand zu halten.

8. Vergleich der Ebenen: was sie wirklich beweisen

Es ist nützlich, alle Ebenen einmal nebeneinander zu sehen – nicht als Sammlung modischer Wörter, sondern als Tabelle von Stärken und Grenzen.

Ebene Was normalerweise wirklich läuft Fängt am besten Was alleine nicht beweist Kosten
Unit eine Klasse, Regel oder ein kleiner Service lokale Geschäftsregeln, Transformationen, Invarianten HTTP-Vertrag, Wiring, echte Datenbank, Security-Chain niedrig
Slice eine Schicht und ihre Infrastruktur JSON-Verträge, Web-Grenze und Verhalten einer konkreten Datenschicht vollständige Anwendungskette und Nachbarschichten niedrig / mittel
Integration mehrere Schichten oder vollständiger Context Wiring, gemeinsames Verhalten von Controller/Service/Repository, Konfiguration Transport-Nuancen eines Live-Servers und Verhalten echter externer Infrastruktur, wenn ersetzt mittel / hoch
Live-Server echter HTTP-Server und Client-Aufruf End-to-End-Web-Verhalten aus Client-Sicht spezifische Besonderheiten einer externen DB oder Integration, wenn nicht wirklich beteiligt hoch
Container Anwendung plus echte externe Komponente PostgreSQL-Verhalten, Migrationen, Infrastruktur-Constraints alles andere „automatisch"; kein universeller Ersatz für die gesamte Testsuite hoch+

Diese Tabelle ist nützlich, weil sie überflüssige Magie beseitigt. Zum Beispiel ist ein Integrationstest nicht „generell besser als ein Unit-Test". Er beweist einfach etwas anderes. Und ein Unit-Test ist nicht „per Definition schwächer als ein Live-Server". Er beantwortet einfach eine andere technische Frage.

9. Ein Benutzerszenario ist nicht gleich ein Test 💥

Hier kommt der Hauptmoment des Tages. Nimm denselben Flow „editor submit → admin approve → public read". Wenn man ihn mit den Augen eines Benutzers betrachtet, ist das eine Geschichte. Wenn man ihn mit den Augen eines Qualitätsingenieurs betrachtet, ist das ein Satz verschiedener Risiken und damit ein Satz verschiedener Beweise.

Die lokale Status-Übergangsregel wird am besten durch einen Unit-Test gefangen. Das richtige 404 beim Public Read prüft man am ehrlichsten an der Web-Grenze. Der Filter nach PUBLISHED lebt in der Datenschicht. Die durchgängige Verbindung Controller → Service → Repository macht auf der Integrations-Ebene Sinn. Der echte HTTP-Austausch – auf dem Live-Server. Die Spezifika von PostgreSQL – auf der Container-Ebene.

Sobald das sichtbar wird, verschwindet die Versuchung, einen „universellen" Testtyp zu suchen. Und genau in diesem Moment hört die gesamte Typisierung auf, bürokratisch zu wirken. Sie beginnt als Karte von Preis und Sicherheit zu funktionieren. Also genau so, wie sie in einem professionellen Backend funktionieren sollte 🚀

10. Typische Fehler im Gespräch über Test-Ebenen 💉

Fehler Nr. 1: Denken, dass ein breiterer Test automatisch besser ist.
Ein breiter Test sieht tatsächlich mehr Umgebung, zahlt dafür aber mit Geschwindigkeit und Diagnosequalität. Er ist nur dort nützlich, wo diese Breite neue Informationen liefert.

Fehler Nr. 2: Die Ebene nach Bibliothek benennen, nicht nach der tatsächlich gestarteten Welt.
Der Dateiname SomethingIntegrationTest beweist noch nichts. Schau auf die tatsächliche Umgebung: Was war echt, was nicht.

Fehler Nr. 3: Von Unit-Tests Garantien für HTTP-Vertrag oder Datenbankverhalten erwarten.
Das ist kein Mangel von Unit-Tests, sondern eine falsche Erwartung. Sie sind gut darin, lokale Risiken schnell und präzise zu fangen.

Fehler Nr. 4: Versuchen, alles über Live-Server oder vollständigen Context zu machen.
Eine solche Suite wird schnell teuer, laut und unhandlich im täglichen Einsatz. Wenn ein Entwickler keine Lust hat, Tests oft auszuführen, hat die Strategie schon verloren.

Fehler Nr. 5: Die Regressions-Suite als „größten Testsatz" wahrnehmen.
Der Regressions-Satz ist nicht wegen seiner Größe wertvoll, sondern wegen seines ROI. Seine Aufgabe ist es, bei Änderungen schnell und regelmäßig ein starkes Signal zu geben, nicht mit der Log-Länge zu beeindrucken.

1
Aufgabe
Spring Test, Level 1, Lektion 2
Gesperrt
Konsolen-Matrix der Teststufen
Konsolen-Matrix der Teststufen
1
Aufgabe
Spring Test, Level 1, Lektion 2
Gesperrt
REST-Teststufen-Referenz
REST-Teststufen-Referenz
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION