CodeGym /Kurse /Spring Boot /@ SpringBootTest: Überprüfung des Boot-Kontexts

@ SpringBootTest: Überprüfung des Boot-Kontexts

Spring Boot
Level 26, Lektion 3
Verfügbar

1. Wann ist @SpringBootTest nötig

Ein leerer Smoke-Test hat bereits eine wichtige Frage beantwortet: Wird der Boot-Kontext überhaupt gestartet? Aber das reicht schnell nicht mehr aus. Man möchte prüfen, ob der benötigte Service tatsächlich aus dem Kontext verfügbar ist, ob CatalogProperties gebunden wurden, ob eine Override-Eigenschaft die Anwendung erreicht hat.

Hier hört @SpringBootTest auf, nur ein Hintergrund für contextLoads() zu sein, und wird zu einem Arbeitswerkzeug für Full-Context-Prüfungen. Und wenn die volle Kontrolle über den Startzeitpunkt, das Profil oder den erwarteten Fehlschlag benötigt wird, ist es bequemer, SpringApplication manuell zu starten.

Wenn die Frage lautet: „Funktioniert ein Enum, ein Value-Object oder ein kleines Stück Java-Logik korrekt?“, wird Spring hier überhaupt nicht benötigt. Aber wenn die Frage lautet: „Wird der catalog-service als Anwendung zusammengesetzt, sind seine Beans verfügbar, wird die Konfiguration gebunden?“, reicht normales JUnit nicht aus. @SpringBootTest wird genau für diese zweite Gruppe von Fragen benötigt.

Das heißt, wir testen nicht eine einzelne Methode, sondern die Anwendung als System: Wiring, @ConfigurationProperties, Validierung, Auto-Konfiguration und die Startmechanik. Daher ist ein solcher Test schwerer als ein normaler JUnit-Test, aber das ist ein fairer Preis für die Überprüfung der Boot-Ebene.

Was macht @SpringBootTest

@SpringBootTest bittet Boot, den ApplicationContext fast so zu starten wie bei einem normalen SpringApplication.run(...). Daher schlagen solche Tests oft schon vor dem ersten assert fehl: Wiring, Binding, Validierung und Startup-Hooks werden vor dem Methodenrumpf ausgeführt.

Auf unserem Niveau reicht das bereits aus, um es von einem leeren Smoke-Test zu unterscheiden. contextLoads() stellt eine allgemeine Frage: „Ist die Anwendung überhaupt am Leben?“. Hier verwenden wir denselben Full-Context bereits für gezieltere Prüfungen.

2. Wie die Anwendung zum Start ausgewählt wird

Wenn Sie @SpringBootTest verwenden, muss Boot verstehen, welche Spring Boot-Anwendung genau gestartet werden soll. Normalerweise findet es die Hauptklasse mit @SpringBootApplication anhand der Paketstruktur. Daher ist es einfacher, die Tests im Basis-Paket com.example.catalogservice oder seinen Unterpaketen zu halten: Dann startet Boot genau die Anwendung, die Sie in der Praxis ausführen.

Wenn Sie die Auswahl der Anwendung explizit machen möchten, können Sie die Klasse direkt in der Annotation angeben:

import com.example.catalogservice.CatalogServiceApplication;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(classes = CatalogServiceApplication.class)
class CatalogBootContextTest {

    @Test
    void bootContextStarts() {
        // Eine scheinbar leere Methode ist hier normal:
        // Die eigentliche Arbeit findet ohnehin beim Start des Kontextes statt.
    }
}

Diese Variante ist besonders nützlich, wenn Sie unnötige Rätsel aus dem Lehrmaterial entfernen möchten: Man sieht sofort, welche Anwendung der Test zu starten versucht.

4. Was im Full-Context gestartet wird

Es ist wichtig zu verstehen, dass ein Full-Context-Test keine „Überprüfung eines einzelnen Beans“ ist. Es ist die Überprüfung, dass die gesamte Maschine zusammengesetzt wird: Ihre @Configuration, @ConfigurationProperties, die Auto-Konfiguration von Boot, das Wiring per Constructor Injection sowie die Infrastrukturkomponenten aus den Startern. Genau deshalb kann ein solcher Test sehr nützliche Dinge aufspüren: zum Beispiel, dass CatalogProperties nicht mehr gebunden wird, weil Sie ein Feld umbenannt haben; oder dass die Validierung den Start verhindert, weil maxFeaturedCount nun nicht null sein darf.

Eine besondere Feinheit ist der Web-Teil. Im catalog-service ist spring-boot-starter-webmvc eingebunden, die Anwendung ist also grundsätzlich webbasiert. Aber @SpringBootTest ist standardmäßig nicht verpflichtet, einen echten eingebetteten Server auf einem Port zu starten, sonst würden Ihre Tests sehr schnell in Portkonflikte geraten und allgemein nervös werden. Normalerweise ist das für grundlegende Kontextprüfungen hervorragend: Es ist wichtig, den Kontext zu laden und zu sehen, dass alle Beans erstellt werden, und nicht den Port 8080 zu öffnen.

@SpringBootTest hat die Einstellung webEnvironment, und es ist hilfreich, zumindest grob zu wissen, was es gibt:

webEnvironment Was passiert Wann ist das sinnvoll
MOCK Startet einen Web-Kontext ohne echten Server Wenn Sie das Wiring der Web-Schicht ohne Port-Start prüfen möchten
NONE Startet einen Nicht-Web-Kontext Wenn der Test überhaupt keine Web-Schicht benötigt
RANDOM_PORT Startet einen Server auf einem zufälligen Port Wenn ein echter HTTP-Start benötigt wird (für unser heutiges Minimum ist das normalerweise überflüssig)

Wir bleiben heute im Paradigma „Kontext geladen – großartig“. Wenn Sie plötzlich in einem Test prüfen möchten, ob der Dienst startet, ob er auf HTTP-Anfragen antwortet, ob das JSON schön ist und ob die Filterung funktioniert – halten Sie inne und atmen Sie tief durch. Das ist nicht mehr das minimale Baseline, sondern der Versuch, mit einem Test die ganze Welt abzudecken. Das ist auch möglich, aber das ist eine andere Ebene und ein anderer Kurs, und ein anderes Maß an Geduld für Ihren Laptop.

5. Minimale Überprüfungen in @SpringBootTest

In dieser Phase haben wir ein maximal pragmatisches Ziel: Wenn sich der catalog-service nicht mehr als Anwendung zusammensetzen lässt (Bean fehlt, Konfiguration wird nicht gebunden, Validierung schlägt fehl), sollte der Test rot werden. Und nicht über komplexe Szenarien, sondern so geradlinig wie möglich.

Beginnen wir mit dem Einfachsten und Nützlichsten: Wir überprüfen, ob CourseCatalogService tatsächlich aus dem Kontext verfügbar ist. Das ist keine „Überprüfung der Geschäftslogik“, sondern eine Überprüfung der Wiring-Ebene. Das heißt, wir bestätigen, dass Boot den Dienst erstellen und ihm Abhängigkeiten injizieren konnte.

import com.example.catalogservice.catalog.service.CourseCatalogService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest
class CourseCatalogServiceBootTest {

    // Bean direkt aus dem Kontext holen: Das ist eine Überprüfung des WIRING, nicht der Geschäftslogik.
    private final CourseCatalogService service;

    // Konstruktor-Injektion im Test macht die Abhängigkeiten explizit und verständlich.
    @Autowired
    CourseCatalogServiceBootTest(CourseCatalogService service) {
        this.service = service;
    }

    @Test
    void serviceIsAvailableFromContext() {
        // Minimaler Smoke-Assert: Der Service wurde erstellt und ist aus dem ApplicationContext verfügbar.
        assertNotNull(service);
    }
}

Hier gibt es eine kleine nette Sache: Wir verwenden Constructor Injection sogar im Test. Die Testklasse ist im herkömmlichen Sinne kein Spring-Bean, aber Spring Test kann Abhängigkeiten injizieren, und dieser Stil zwingt Sie, die Abhängigkeiten explizit zu halten (und nicht „irgendwo durch Feldinjektion hereingeflogen“).

Jetzt überprüfen wir die zweite wichtige Schicht unseres Projekts: die typsichere Konfiguration. CatalogProperties ist das Steuerzentrum für app.catalog.*. Wenn es nicht mehr gebunden wird, kann die Anwendung entweder beim Start abstürzen oder sich seltsam verhalten. In beiden Fällen ist es besser, dies in Tests zu erfahren.

import com.example.catalogservice.config.CatalogProperties;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest
class CatalogPropertiesBootTest {

    // Überprüfen, ob die typisierte Konfiguration überhaupt gebunden wird und im Kontext landen kann.
    private final CatalogProperties properties;

    @Autowired
    CatalogPropertiesBootTest(CatalogProperties properties) {
        this.properties = properties;
    }

    @Test
    void titleIsNotBlank() {
        // Basis-Invariante: Der Titel darf nach dem Binding der Konfiguration nicht leer sein.
        assertTrue(!properties.title().isBlank());
    }
}

Beachten Sie die Art der Assertions: Sie sind kurz und versuchen nicht, „alles auf der Welt“ zu beweisen. Wir testen nicht, dass „die Kursfilterung perfekt ist“. Wir testen, dass der Kontext gestartet wurde und die Basisinvarianten der Konfiguration vernünftig aussehen.

6. Inline-Test-Properties

Einer der nützlichsten Tricks von @SpringBootTest auf minimalem Baseline-Niveau ist die Möglichkeit, Eigenschaften direkt in die Annotation einzufügen. Das ist besonders angenehm in einem konfigurationsorientierten Projekt wie catalog-service: Wir können überprüfen, ob eine Eigenschaft tatsächlich beeinflusst, was in CatalogProperties gebunden wird, ohne application.yaml anzurühren und ohne „lass uns für einen Test die Projektkonfiguration ändern“ zu veranstalten.

Die Syntax sieht so aus: @SpringBootTest(properties = "..."). Normalerweise werden Eigenschaften im Format key=value angegeben. In der Praxis können Sie eine einzelne Eigenschaft als Zeichenkette oder mehrere als Array von Zeichenketten übergeben.

Überprüfen wir, ob eine Inline-Eigenschaft tatsächlich den Wert aus YAML überschreibt und die typisierte Konfiguration erreicht:

import com.example.catalogservice.config.CatalogProperties;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertFalse;

@SpringBootTest(properties = "app.catalog.startup-report-enabled=false")
class CatalogPropertiesOverrideBootTest {

    // In diesen Testkontext legen wir speziell eine Override-Eigenschaft.
    private final CatalogProperties properties;

    @Autowired
    CatalogPropertiesOverrideBootTest(CatalogProperties properties) {
        this.properties = properties;
    }

    @Test
    void inlinePropertyOverridesYaml() {
        // Wir testen genau den Priorisierungsmechanismus: Der Wert aus dem Test sollte YAML überschreiben.
        assertFalse(properties.startupReportEnabled());
    }
}

Warum ist das wichtig? Weil es die reale Lebensumgebung einer Boot-Anwendung modelliert: Zur Laufzeit können Eigenschaften nicht nur aus application.yaml, sondern auch über Umgebungsvariablen, Systemeigenschaften, CLI-Argumente usw. kommen. Inline-Test-Properties sind eine „Lern-Sandbox“ für diese Idee: Ohne die Projektdateien zu ändern, schaffen Sie eine kontrollierte Umgebung für den Test.

Eine weitere kleine Nuance, die man gerne weiß: Solche Eigenschaften haben eine hohe Priorität. Wenn in application.yaml etwas anderes steht und Sie im Test etwas anderes einfügen, gewinnt für diesen Testkontext der Wert aus dem Test. Genau das, was wir brauchen: Der Test sollte autark sein und sein Szenario steuern.

Wenn mehrere Eigenschaften auf einmal benötigt werden, macht man es normalerweise so:

@SpringBootTest(properties = {
        // Mehrere Overrides in einem Test: Praktisch für kleine „Konfigurationsszenarien“.
        "app.catalog.title=Test Catalog",
        "app.catalog.max-featured-count=2"
})
class CatalogMultipleOverridesBootTest {
}

Und hier kommt ein wichtiger betrieblicher Gedanke: Jede eindeutige Kombination aus Kontext und Eigenschaften kann einen separaten Kontext erzeugen. Spring versucht, Kontexte zwischen Tests zu cachen, aber wenn Sie zehn Testklassen haben und jede andere Inline-Properties setzt, werden Sie die Anwendung faktisch zehnmal starten. Daher sind Inline-Properties zwar mächtig, aber sollten gezielt und bewusst eingesetzt werden, nicht „einfach so in jedem Test“.

7. Wie man @SpringBootTest sorgfältig einsetzt

Ein vollständiger Kontext ist, als würde man die Lieferung der gesamten Restaurantkarte bestellen, um zu prüfen, ob die Suppe salzig ist. Es funktioniert zwar, aber es wird teuer. Daher ist es auf minimalem Baseline-Niveau wichtig, zwei Ideen gleichzeitig zu verfolgen: @SpringBootTest gibt uns eine sehr wertvolle Überprüfung, dass die Anwendung zusammengesetzt wird, aber wenn wir anfangen, damit alle Aufgaben zu lösen, werden die Tests langsam, laut und beginnen zu nerven (und nervige Tests haben die unangenehme Eigenschaft, dass sie deaktiviert werden).

Eine pragmatische Strategie für unseren Kurs sieht so aus: Full-Context-Tests schreiben wir dort, wo es wirklich um den Zusammenbau der Anwendung geht. Das sind Überprüfungen auf der Ebene „Bean wird erstellt“, „Konfiguration wird gebunden“, „Kontext startet“. Für alles andere (z. B. zur Überprüfung reiner Java-Logik von Enums, Hilfsprogrammen oder kleinen Funktionen) verwenden wir normale JUnit-6-Tests ohne Spring.

Eine weitere wichtige Gewohnheit ist, @SpringBootTest nicht in einen „Test für alles auf einmal“ zu verwandeln. Wenn Sie zehn Beans in die Testklasse injiziert und zwanzig Assertions gemacht haben, haben Sie bei einem Fehlschlag zwei Möglichkeiten: entweder lange herauszufinden, was genau kaputt ist, oder traurig auf den roten Test zu schauen und zu denken „na ja, irgendwas funktioniert da nicht“. Viel besser ist es, wenn der Test eine einzige menschliche Frage beantwortet. Zum Beispiel: „Kann Boot einen CourseCatalogService erstellen?“ oder „Wird CatalogProperties gebunden und akzeptiert Override?“

Und das Letzte, was man ansprechen sollte: Ja, @SpringBootTest wird oft Logs ausgeben. Das ist normal, weil die Anwendung tatsächlich innerhalb des Tests gestartet wird. Wenn die Logs stören, wird das normalerweise durch Anpassen der Logging-Level behoben (aber wir werden die heutige Vorlesung nicht zu einem „Logback-Battle“ machen, wir haben ein anderes Ziel).

8. Typische Fehler bei der Arbeit mit @SpringBootTest

Fehler Nr. 1: @SpringBootTest zum Testen einfacher Java-Logik verwenden.
Das ist der häufigste „Gütigkeitsfehler“: Der Student möchte gut dastehen und entscheidet, dass, da wir Spring Boot lernen, auch die Tests „alle mit Spring“ sein sollten. Infolgedessen werden die Tests langsam, und Sie überprüfen etwas, das auch mit normalem JUnit 6 in Millisekunden überprüft werden kann. Wenn Sie eine Methode testen, die überhaupt nicht vom Spring-Kontext abhängt, ist es besser, den Kontext nicht zu starten – das spart Zeit und macht den Test einfacher.

Fehler Nr. 2: Erwarten, dass @SpringBootTest „fast wie ein Unit-Test, nur mit Annotation“ ist.
In der Praxis ist es ein Integrationstest auf Kontextebene. Er startet viel Infrastruktur, und sein Fehlschlag kann bereits vor dem Eintritt in die @Test-Methode erfolgen. Anfänger erschreckt das: „Ich bin noch nicht einmal zu den Assertions gekommen.“ Aber genau so soll es sein: Wenn der Kontext nicht zusammengesetzt werden kann, gibt es nichts weiter zu testen – die Anwendung startet nicht.

Fehler Nr. 3: Die Testklasse außerhalb des Basispakets der Anwendung zu platzieren und „@SpringBootConfiguration nicht gefunden“ zu erhalten.
Das sieht aus wie „Ich habe doch nur einen Test erstellt“, aber Boot antwortet: „Ich weiß nicht, was ich starten soll“. Behoben wird dies entweder durch die richtige Platzierung der Tests (innerhalb von com.example.catalogservice) oder durch die explizite Angabe von classes = CatalogServiceApplication.class. In einem Lehrprojekt ist es am einfachsten, die Tests diszipliniert im selben Paketbaum wie die Anwendung zu halten.

Fehler Nr. 4: In einem einzigen @SpringBootTest die gesamte Anwendung auf einmal zu überprüfen.
Die Versuchung ist verständlich: Da wir den Kontext gestartet haben, „testen wir nebenbei“ auch den Service, das Repository, die Konfiguration und noch ein paar Filter. Aber dann schlägt dieser Test fehl, und Sie verstehen nicht, was genau. Besser ist es, zwei oder drei kurze Tests mit einem einzigen Ziel zu haben als einen großen Test-Combine-Harvester, der nur „alles schlecht“ meldet.

Fehler Nr. 5: Inline-Properties so zu missbrauchen, dass jeder Test einen neuen Kontext startet.
Inline-Test-Properties sind ein hervorragendes Werkzeug. Aber wenn Sie in jeder Testklasse einen einzigartigen Satz von Eigenschaften definieren, kann Spring den Kontext-Cache nicht effektiv wiederverwenden, und die Tests werden anfangen, sich wie ein Toaster auf Maximum aufzuheizen. Auf Baseline-Ebene reichen normalerweise ein oder zwei Szenarien mit Overrides aus, um zu zeigen, dass der Mechanismus funktioniert, und um die Suite nicht in einen Marathon zu verwandeln.

1
Aufgabe
Spring Boot, Level 26, Lektion 3
Gesperrt
Vollständiger Boot-Kontext und Verfügbarkeit des Service-Beans
Vollständiger Boot-Kontext und Verfügbarkeit des Service-Beans
1
Aufgabe
Spring Boot, Level 26, Lektion 3
Gesperrt
Inline-Eigenschaften ueberschreiben den Wert einer typsicheren Konfiguration
Inline-Eigenschaften ueberschreiben den Wert einer typsicheren Konfiguration
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION