1. Rolle von health neben Web Smoke
Wenn Sie zum ersten Mal hören: „Lassen Sie uns /actuator/health testen“, ist es leicht, das als „einen weiteren Endpoint, na und“ aufzufassen. Aber Health ist kein gewöhnlicher API-Endpoint und auch kein „weiteres JSON“. Es ist ein formelles Signal über den Zustand des Dienstes: dass er nicht nur gestartet ist, sondern bereit ist, nützlich zu sein. Beantwortet der Web Smoke die Frage „Antwortet der Server überhaupt?“, so beantwortet Health die Frage „Antwortet er bei klarem Verstand?“.
Im catalog-service ist dies besonders deutlich, da unser Dienst read-only ist und von Konfigurationsdaten lebt. Theoretisch kann die Anwendung starten, die Controller können erreichbar sein, aber tatsächlich kann der Dienst „leer“ oder „fragwürdig“ sein: zum Beispiel, wenn der Katalog ohne veröffentlichte Kurse geladen wurde oder widersprüchliche Daten vorhanden sind. Dem Nutzer nützt ein solcher „lebendiger, aber nutzloser“ Dienst nichts.
Genau deshalb haben wir im Projekt den CatalogDataHealthIndicator erstellt: er prüft weder das Framework noch das Netzwerk, sondern domänenverständliche Bedingungen. In unserer Formulierung ist der Dienst „gesund“, wenn der Katalog geladen wurde, mindestens ein veröffentlichter Kurs vorhanden ist und es keine doppelten slug gibt. Das ist ähnlich wie die Prüfung „das Auto hat einen Motor, Benzin und die Räder fallen nicht ab“ – keine Garantie, dass Sie in den Urlaub fahren, aber ohne das geht es gar nicht.
Wichtig ist auch, dass Health eine Sache ist, die in der Praxis oft von Automatisierung genutzt wird: Monitoring, Load Balancer, Orchestratoren. Wir gehen nicht in Kubernetes und Production Hardening (das sind separate Disziplinen), aber es ist bereits jetzt nützlich, „Health als Vertrag“ zu denken – damit der Dienst nicht nur ein Lehrbeispiel, sondern auch technisch vernünftig ist.
2. Modell: HealthIndicator → Health → Endpoint
Um Health sicher zu testen, darf man nicht raten, sondern muss die Kette verstehen: wer ruft was auf und wo erscheint JSON. In Spring Boot Actuator gibt es eine Schicht von Indikatoren, die ein Health-Objekt zurückgeben (mit Status und Details), und eine Endpoint-Schicht, die die Ergebnisse verschiedener Indikatoren sammelt und die endgültige Antwort ausgibt. Wenn man dieses Schema im Kopf behält, versteht man leicht, was genau man in jedem Test testet.
Vereinfacht kann man es so darstellen:
flowchart TD
A[GET /actuator/health] --> B[Actuator Health Endpoint]
B --> C[DiskSpaceHealthIndicator]
B --> D[CatalogDataHealthIndicator]
C --> E[Health: UP/DOWN + details]
D --> F[Health: UP/DOWN + details]
E --> G[Aggregated Health]
F --> G
G --> H[JSON response]
Es gibt drei „Realitätsebenen“:
1) Unser Indikator (CatalogDataHealthIndicator) – das ist die reine Logik, die wir selbst geschrieben haben. Er gibt Health zurück, normalerweise über Builder Health.up() oder Health.down().
2) Actuator-Aggregator – das ist der Framework-Teil, der die Beiträge verschiedener Indikatoren sammelt und den Gesamtstatus erstellt. Wir sind nicht verpflichtet, die „Korrektheit des Aggregators“ zu testen – das ist Aufgabe von Spring Boot, und das macht es seit vielen Jahren.
3) Web-Darstellung /actuator/health – das ist das, was die Außenwelt sieht. Hier kommen die Einstellungen für die Endpoint-Exposition, den Base Path sowie Einstellungen wie „Details anzeigen“ ins Spiel.
Es ist nützlich, eine kleine Tabelle festzuhalten, um die Schichten nicht zu verwechseln:
| Was wir prüfen | Wo es lebt | Was ein Ausfall bedeutet |
|---|---|---|
| indicator.health() gibt UP zurück | in unserem Code | Domänenlogik von Health oder Abhängigkeit des Indikators defekt |
| GET /actuator/health antwortet und enthält UP | in der Web-Schicht von Actuator | Endpoint nicht gestartet/nicht exponiert/Integration defekt oder Gesamthealth ist nicht UP |
Eine kleine Nuance, die Anfänger oft überrascht: Status in Actuator ist kein „irgendein String“, sondern ein Objekt (z.B. Status.UP). Das JSON im Endpoint enthält jedoch normalerweise den String-Wert "UP". Daher vergleichen Sie im Code Status.UP, im JSON-Test aber den String "UP".
3. Test des Indikators direkt
Der reinste Teil des Health-Testens ist es, den Indikator direkt zu prüfen, ohne HTTP, MockMvc und JSON-Format. Das ist ein nahezu idealer „Engineer-Kompromiss“: der Test bleibt Boot-zentriert (wir starten den Kontext, erhalten echte Beans), prüft aber das, was uns gehört – das Ergebnis von health().
Beginnen wir mit einer minimalen Testvorlage, bei der wir einfach den Kontext starten und unseren Indikator injizieren.
Datei: src/test/java/com/example/catalogservice/actuator/CatalogDataHealthIndicatorTest.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest // Starten den Spring-Kontext, um echte Beans zu erhalten
class CatalogDataHealthIndicatorTest {
@Autowired
CatalogDataHealthIndicator indicator; // Testen genau unseren HealthIndicator
}
Jetzt fügen wir das einfachste positive Szenario hinzu: Bei normaler Projektkonfiguration sollte der Indikator UP melden. Dieser Test handelt nicht davon, wie viele Kurse Sie genau im Konfig haben, in welcher Reihenfolge sie liegen oder welche Felder in JSON serialisiert werden. Es ist ein Test über die grundlegende Garantie: „Der Katalog ist in Ordnung“.
Datei: CatalogDataHealthIndicatorTest.java (Auszug)
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Status;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Test
void reportsUpForValidCatalog() {
// Prüfen das Domänenergebnis: Service "bereit", also sollte Status UP sein
assertEquals(Status.UP, indicator.health().getStatus());
}
Beachten Sie den Stil der Assertions: wir vergleichen Status.UP, nicht "UP". Das ist eine wichtige Kleinigkeit: Sie testen die Java-Logik und den API-Vertrag von Actuator, nicht die JSON-Darstellung.
Über Health: was man prüfen sollte
Wenn Sie indicator.health() aufrufen, erhalten Sie Health. Es hat getStatus() und getDetails(). In die details legen wir oft nützliche Domänen-Hinweise: wie viele veröffentlichte Kurse, ob es Duplikate gibt, ob der Katalog geladen ist. Das ist praktisch für die Diagnose, aber es gibt eine Falle: wenn Sie anfangen, alle Details bis zum letzten Schlüssel zu prüfen, wird der Test zerbrechlich.
Ein guter Ansatz für Anfänger ist, entweder nur den Status zu prüfen oder ein oder zwei stabile Fakten. Zum Beispiel: „in den Details gibt es den Schlüssel publishedCourses und der Wert größer als Null“. Das bindet den Test nicht an die genaue Anzahl der Kurse und zwingt Sie nicht, den Test jedes Mal zu ändern, wenn Sie einen weiteren Kurs im Konfig hinzufügen.
Datei: CatalogDataHealthIndicatorTest.java (Auszug)
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Test
void includesDetailsWeOwn() {
Health health = indicator.health(); // Rufen den Indikator direkt auf, ohne HTTP-Schicht
// Details sind Diagnoseinformationen, die wir im Rahmen des Indikators "besitzen"
assertTrue(health.getDetails().containsKey("publishedCourses")); // Schlüssel vorhanden
}
Wenn Sie in Ihrem CatalogDataHealthIndicator keinen solchen Detail-Schlüssel haben – kein Problem, dieses Fragment zeigt nur die Idee. Der Sinn ist, dass man diejenigen „Details“ prüfen sollte, die Sie als Teil des Vertrags Ihres Indikators betrachten, und nicht alle Interna hintereinanderweg.
Warum ist es nützlich, sowohl einen Test des Indikators direkt als auch einen Endpoint-Smoke zu haben? Denn wenn der Test auf indicator.health() fehlschlägt, verstehen Sie sofort den Problembereich: Es ist Ihre Logik von Health oder die Abhängigkeit, auf die sie sich stützt. Sie müssen nicht raten: „Ist es MockMvc? Ist es Serialisierung? Ist es der Base Path? Ist es die Exposure Policy?“. Sie schauen auf den Indikator, Punkt.
Das ähnelt der Autodiagnose: wenn Sie die Batterie separat geprüft haben und sie tot ist, müssen Sie nicht Radio, Scheinwerfer und Scheibenwischer prüfen. Ja, alles funktioniert nicht – aber die Ursache ist lokalisiert.
4. Smoke-Test von /actuator/health über MockMvc
Den Indikator direkt zu prüfen ist gut, aber das reicht nicht aus, um sich der „Betriebsoberfläche“ des Dienstes sicher zu sein. Man kann versehentlich Actuator deaktivieren, den Endpoint durch Exposure-Einstellungen blockieren, den Base Path oder den Management-Port ändern. Am Ende kann der Indikator perfekt sein, aber die Außenwelt sieht ihn nie. Daher fügen wir einen zweiten Test hinzu: einen leichten Endpoint-Smoke.
Beginnen wir mit der Vorbereitung des Tests mit MockMvc. Das ist alles aus der vorherigen Lektion bekannt: @SpringBootTest + @AutoConfigureMockMvc, und wir erhalten ein MockMvc-Objekt, um den Endpoint ohne echtes Netzwerk aufzurufen.
Datei: src/test/java/com/example/catalogservice/actuator/HealthEndpointSmokeTest.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest // Starten den Anwendungskontext
@AutoConfigureMockMvc // Aktivieren MockMvc, um Endpoints ohne echten HTTP-Server anzusprechen
class HealthEndpointSmokeTest {
@Autowired
MockMvc mockMvc; // Damit machen wir GET /actuator/health
}
Jetzt der Test selbst. Hier ist es wichtig, sich zurückzuhalten (im guten Sinne): wir machen keinen „vollständigen JSON-Snapshot“ und vergleichen ihn nicht vollständig. Für einen Smoke reicht es zu prüfen, dass der Endpoint erreichbar ist und der Gesamtstatus "UP" ist.
Datei: HealthEndpointSmokeTest.java (Auszug)
import org.junit.jupiter.api.Test;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Test
void exposesHealthEndpoint() throws Exception {
mockMvc.perform(get("/actuator/health")) // Rufen den Actuator-Endpoint genauso auf, wie es die Außenwelt tun würde
.andExpect(status().isOk()) // Im gesunden Zustand normalerweise 200 OK
.andExpect(jsonPath("$.status").value("UP")); // Hier prüfen wir die JSON-Repräsentation, daher String
}
jsonPath("$.status") statt contains("UP")
In früheren Beispielen wird oft body.contains("UP") verwendet, und für den allerersten Kontakt ist das okay. Sobald jedoch zusätzliche Felder in der Antwort erscheinen oder Sie Health-Komponenten hinzufügen, kann die String-Suche weniger zuverlässig werden. jsonPath("$.status") sagt direkt: „mich interessiert das spezifische Feld status“, und der Test bleibt lesbar.
Nuance zum HTTP-Status und 200 OK
Im gesunden Zustand des Dienstes ist der Gesamthealth normalerweise UP und Actuator antwortet mit 200. Sollte der Gesamthealth DOWN werden, kann Actuator 503 Service Unavailable zurückgeben (und das ist übrigens sinnvoll: „Ich lebe, aber ich bin nicht bereit“). Wir bauen jetzt keine komplexe Matrix negativer Szenarien auf, aber es ist wichtig zu merken: nicht immer health = 200. Hängt vom Gesamtstatus und den Einstellungen ab.
5. Grenzen von Health-Tests
Jede Testsuite stirbt nicht an zu wenigen Tests, sondern an ihrem „unkontrollierten Ausufern“. Health-Tests sind besonders anfällig dafür: Sie öffnen /actuator/health, sehen ein schönes JSON, und die Hand greift danach, alles zu prüfen: Disk Space, Uptime, Komponenten, Details… Und schon testen Sie Dinge, die Sie nicht geschrieben haben und die sich von Umgebung zu Umgebung ändern können.
Der gesündeste (verzeihen Sie das Wortspiel) Ansatz ist, sich an die Verantwortungsgrenzen zu erinnern. Unsere Tests sollten zwei konkrete Fragen beantworten. Erste Frage: „Stellt unser CatalogDataHealthIndicator tatsächlich korrekt den Zustand des Katalogs fest?“. Zweite Frage: „Ist der Endpoint /actuator/health tatsächlich erreichbar und gibt den Gesamtstatus UP zurück?“. Alles, was tiefer geht, gehört entweder zu einem separaten Testkurs oder macht die Tests schwerfällig und zerbrechlich.
Damit diese Grenze vollends greifbar ist, behalten Sie eine einfache Entschlüsselung der Fehler im Kopf:
| Test gefallen | Was es in der Praxis bedeutet | Wahrscheinlichster Problemort |
|---|---|---|
| CatalogDataHealthIndicatorTest | Katalog-Gesundheitsregel oder Abhängigkeit defekt | Indikatorcode, Service/Repository, Datenkonfiguration |
| HealthEndpointSmokeTest | Endpoint nicht erreichbar oder Gesamthealth nicht UP | Exposure/Base-Path/Actuator-Verdrahtung oder echtes Gesundheitsproblem |
Noch eine feine Nuance: versuchen Sie nicht, den Health-Test „über die Geschäftslogik des Katalogs“ zu machen. Wenn Sie im Health-Test anfangen, die Filterung von Kursen, die Verarbeitung von Query-Parametern und andere Dinge zu prüfen – vermischen Sie die Ebenen. Health sollte nicht eine zweite API für den Katalog sein, und der Health-Test sollte sich nicht in einen alternativen Controller-Test verwandeln.
Und schließlich, wenn Sie unbedingt ein negatives Health-Szenario testen möchten (z.B. „keine veröffentlichten Kurse ⇒ DOWN“), machen Sie das maximal lokal und verständlich. Idealerweise schaffen Sie im Test kontrollierte Bedingungen (z.B. indem Sie dem Indikator Testdaten über eine Abhängigkeit unterjubeln) und prüfen nur den Status. Aber auf dem Niveau dieses Kurses halten wir uns an ein minimales Set: ein positiver Test auf den Indikator und ein Smoke auf den Endpoint – das ist bereits eine hervorragende Basisschicht an Vertrauen.
6. Typische Fehler bei Health-Tests
Fehler Nr. 1: Nur /actuator/health prüfen und den CatalogDataHealthIndicator selbst ignorieren.
Wenn Sie nur den Endpoint testen, unterscheiden Sie nicht zwischen zwei Situationen: „Ihre Health-Logik ist defekt“ und „Endpoint nicht exponiert/nicht verbunden“. Infolgedessen erhalten Sie bei Testfehlschlag ein unscharferes Signal und beginnen, das Problem im gesamten Projekt zu suchen. Die direkte Prüfung von indicator.health() macht die Fehlerursache viel lokaler und verständlicher.
Fehler Nr. 2: Die gesamte JSON-Antwort von /actuator/health vollständig vergleichen.
Das vollständige JSON ändert sich leicht: Komponenten können hinzugefügt werden, Details ändern sich, neue Indikatoren von Boot können erscheinen oder die Konfiguration von show-details kann sich ändern. Wenn Sie die gesamte Antwort als String vergleichen, wird der Test zu einem „Wachhund“ gegen jede Änderung, selbst gegen nützliche. Viel stabiler ist es, nur $.status und maximal ein oder zwei stabile Elemente zu prüfen, die Sie tatsächlich als Vertrag betrachten.
Fehler Nr. 3: Versuchen, über Health die Geschäftslogik des Katalogs zu testen.
Health ist ein Signal „bereit/nicht bereit“, nicht „Liste der Kurse korrekt nach Track gefiltert“. Sobald in Health-Tests Prüfungen der Filterung, Sortierung oder der Antwortform des Katalogs auftauchen, vermischen Sie die Verantwortlichkeiten. Das führt fast immer dazu, dass die Tests überladen werden und „fehl am Platz“ fehlschlagen.
Fehler Nr. 4: Erwarten, dass Health immer 200 OK zurückgibt.
Im Zustand UP – ja, normalerweise 200. Sobald der Gesamtstatus jedoch DOWN wird, kann Actuator mit 503 antworten. Wenn Sie jemals negative Szenarien hinzufügen, machen Sie keine Prüfung „immer muss 200 sein“. Prüfen Sie das, was Sie wirklich wollen: den Status in JSON oder den erwarteten HTTP-Status für einen bestimmten Zustand.
Fehler Nr. 5: Details des Health-Endpoints prüfen, ohne die Einstellungen von show-details zu kontrollieren.
Selbst wenn Ihr Indikator nützliche Details in Health ablegt, kann der Endpoint sie möglicherweise nicht anzeigen (und das ist oft gewünscht, besonders außerhalb von local/dev). Daher kann die Prüfung von Details über /actuator/health instabil und profIlabhängig sein. Wenn Sie Details unbedingt benötigen, prüfen Sie sie direkt über indicator.health().getDetails() und betrachten Sie das als Vertrag Ihres Indikators, nicht der Web-Antwort.
GO TO FULL VERSION