1. Separate Konfiguration für Tests
Wenn Sie gerade erst anfangen, könnte es scheinen, dass ein Test „einfach nur Code ist, der ausgeführt wird“. Aber ein Spring-Boot-Test ist ein kleiner Start der Anwendung, daher liest er Einstellungen: Pfade zu Dateien, URLs externer Dienste, Limits, Verhaltensflags. Wenn diese Einstellungen zufällig „produktiv“ sind, können Tests sehr seltsame Dinge tun: Dateien an die falsche Stelle schreiben, versuchen, einen echten externen Dienst zu erreichen oder eine Funktionalität aktivieren, die Sie in Tests überhaupt nicht sehen wollten.
Stellen Sie sich vor, Ihr ContentHub speichert Artikelanhänge auf der Festplatte. In der Produktion kann dies ein ernsthafter Pfad sein, lokal hingegen Ihr Arbeitsverzeichnis. Wenn die Tests gedankenlos den „Produktionspfad“ übernehmen, beginnen sie, Dateien dort zu erstellen, wo sie nichts zu suchen haben. Und das ist keine Theorie: Solche Situationen äußern sich oft in dem Satz „Bei mir laufen die Tests, aber bei meinem Kollegen ist alles kaputt“, weil der Kollege in seiner Umgebung andere Werte eingefangen hat.
Der zweite Grund ist Wiederholbarkeit. Eine gute Test-Suite sollte wie ein Pfannkuchenrezept sein: Wenn du alles Schritt für Schritt machst, gelingen die Pfannkuchen bei dir, bei deinem Freund und auf CI. Wenn die Tests jedoch von der „Raumtemperatur“ abhängen (in unserer Welt von Umgebungsvariablen, zufälligen Werten, Pfaden und URLs), dann sind das keine Tests mehr, sondern Kaffeesatzleserei: mal „ja“, mal „oh“.
Deshalb beginnen wir die verwaltete Testkonfiguration mit einem einfachen Prinzip: Tests sollten einen einzigen klaren Basismodus haben, der explizit eingeschaltet und durch eine einzige Datei festgelegt wird.
Den Typ des Spring-Tests auszuwählen ist nur die halbe Miete. Danach muss diesem Test eine vorhersagbare Umgebung gegeben werden: zuerst eine sichere Baseline für alle Läufe, dann seltene lokale Overrides für einzelne Szenarien, und wenn ein Wert erst zur Startzeit entsteht, dann sind dynamische Properties nützlich. Wenn die Properties zahlreich werden, sammeln wir sie zusätzlich in typisierten Objekten und achten darauf, dass nicht der gesamte Wiring geändert werden muss, statt einen Wert „anzupassen“. Es beginnt alles mit dem Einfachsten – einem separaten Test-Profil.
2. Profile und application-*.yml
Um sicher mit der Testkonfiguration zu arbeiten, muss man aufhören, YAML als „magischen Text“ zu betrachten, und darin ein recht einfaches Modell sehen. Spring Boot sammelt Properties aus verschiedenen Quellen und legt sie in einer endgültigen Menge ab. Danach liest die Anwendung Werte anhand von Schlüsseln wie contenthub.storage.root. Ein Profil ist einfach eine Möglichkeit zu sagen: „Füge für diesen Modus zusätzlich zu den Basiseinstellungen noch diese Werte hinzu.“
In Spring Boot sieht die Idee der Profile so aus: Es gibt eine Basiskonfiguration, normalerweise in application.yml. Sie legt die „Standard“-Werte fest. Wenn dann das Profil test aktiviert ist, lädt Boot zusätzlich application-test.yml und legt es über die Basisdatei. Wenn der Schlüssel übereinstimmt, überschreibt der Wert aus der Profildatei den Basiswert. Wenn der Schlüssel nicht vorhanden ist, bleibt der Basiswert.
Sehr wichtig: Profile sind nicht „verschiedene Anwendungen“. Es ist dieselbe Anwendung, aber in verschiedenen Modi. Das passt perfekt zu uns: Tests sollen denselben ContentHub starten, aber mit sicheren Einstellungen. Nicht mit einem „falschen ContentHub“, nicht mit einer „separaten Testversion“, sondern mit demselben Code, nur mit anderen Konfigurationswerten.
Es ist nützlich, dies als kurzes Schema zu sehen:
flowchart TD
%% Schema der Überlagerung von Basiskonfiguration und Test-Profil
A["application.yml
(Basis)"] --> C["endgültiger Property-Satz
Environment"]
B["application-test.yml
(Profil test)"] --> C
C --> D["Test liest Wert
${contenthub.storage.root}"]
Dieses Bild ist die Hauptbotschaft der Vorlesung. Wir „erfinden“ keine neuen Einstellungen, sondern stellen nur sicher, dass in Tests garantiert sichere Werte angewendet werden.
Wo liegen die Konfigs: main und test resources
Mit der Konfiguration kann man leicht Chaos anrichten, wenn man nicht versteht, welche Ressourcen „wirklich“ in die Anwendung gelangen und welche nur in Tests. In einem Gradle/Spring-Boot-Projekt haben wir normalerweise zwei wichtige Ressourcenverzeichnisse: src/main/resources und src/test/resources. Das erste gelangt in das endgültige Jar und wird beim normalen Start der Anwendung verwendet. Das zweite gelangt nur in den Test-Classpath und ist während der Tests sichtbar.
Wenn man application-test.yml in src/main/resources ablegt, mag das formal funktionieren, aber es entsteht eine fragwürdige Situation: Die Testdatei landet im Artefakt der Anwendung. Das ist selten tödlich, aber fast immer überflüssig. Uns ist es viel beruhigender, die Testkonfiguration dort zu halten, wo sie hingehört – in den Testressourcen.
In Form eines kleinen Baums (wie bei einer Durchleuchtung des Projekts) sieht das so aus:
src
├─ main/resources
│ └─ application.yml
└─ test/resources
└─ application-test.yml
Für ContentHub ist das besonders wichtig, weil die Testkonfiguration normalerweise Dinge wie lokale Dateispeicherpfade und das Deaktivieren einiger Nebeneffekte enthält. Das sind keine „Geheimnisse“, aber auch nichts, was man versehentlich in das Produktions-Jar schleppen möchte.
3. application-test.yml: nur Unterschiede
Eine gute Testkonfiguration ist keine Kopie der Hauptkonfiguration, sondern eine saubere Schicht darüber. Stellen Sie sich vor, Sie haben ein Basisrezept (application.yml), und das Testprofil ist eine kleine Anpassung: „statt Butter Margarine“, „statt Zucker weniger Zucker“. Wenn Sie das gesamte Rezept neu schreiben, dann weichen die Rezepte früher oder später voneinander ab, und niemand versteht mehr, was stimmt.
Damit verbunden ist die praktischste Empfehlung: application-test.yml sollte nur die Schlüssel enthalten, die sich wirklich für Tests unterscheiden. Dann sieht man sofort den Sinn der Datei: Sie sorgt für die Sicherheit der Testumgebung, anstatt zu versuchen, ein „zweites application.yml“ zu werden.
Schauen wir uns ein vereinfachtes Beispiel einer Basisdatei an. Sie kann in src/main/resources/application.yml leben und „normale“ Werte enthalten, die nicht unbedingt produktionsrelevant, aber für die Anwendung logisch sind:
# src/main/resources/application.yml
contenthub:
storage:
root: "build/storage" # Basis-Pfad für Dateispeicher (normaler Anwendungsstart)
moderation:
base-url: "http://localhost:8080" # Basis-URL des Moderationsdienstes
notifications:
enabled: true # Im normalen Modus sind Benachrichtigungen aktiviert
Nun das Testprofil. Es lebt in src/test/resources/application-test.yml und ändert nur das, was für Tests kritisch ist:
# src/test/resources/application-test.yml
contenthub:
storage:
root: "build/test-storage" # Test-Pfad innerhalb von build/, sodass clean alles löschen kann
moderation:
base-url: "http://localhost:8089" # Sicherer lokaler Adress für Tests
notifications:
enabled: false # Deaktivieren von Nebeneffekten (Benachrichtigungen) in Tests
Beachten Sie den „Charakter“ dieser Werte. Der Pfad für Dateien liegt innerhalb von build/, also innerhalb des Arbeitsverzeichnisses des Projekts. Wir stören niemanden, beschädigen nichts, und ./gradlew clean fegt bei Bedarf alles wie ein Bulldozer weg. notifications.enabled: false – auch das ist ein sehr charakteristisches Testverhalten: Tests sollten sich nicht zufällig wie eine „echte“ Anwendung verhalten, die Benachrichtigungen versendet.
Ein weiterer subtiler, aber wichtiger Punkt: Wir verwenden dieselben Schlüssel wie die Anwendung im Normalmodus. Das bedeutet, dass der Produktionscode unverändert bleibt: Er liest immer noch contenthub.storage.root, nur die Quelle des Werts ist in Tests eine andere.
4. Was sollte man zuerst in das Test-Profil auslagern?
Wenn Sie das Projekt ContentHub mit Testeraugen betrachten, besonders in einer frühen Phase, interessieren Sie nicht die „schönen Einstellungen“, sondern die Risikopunkte: Was kann Nichtdeterminismus erzeugen, was kann I/O verursachen, was kann versuchen, nach außen zu gehen? Genau solche Dinge sollten zuerst in application-test.yml wandern, weil sie fast jeden Test mit einem gestarteten Spring-Kontext beeinflussen.
Für unsere Domäne ist es praktischer, nicht in einer Liste „Schlüssel um der Schlüssel willen“ zu denken, sondern in einer kleinen Tabelle „Risiko → Property → warum im Test-Profil“. Dann wird die Konfiguration nicht mehr zu einer Sammlung von Zeilen, sondern zu einem Teil der Ingenieurslogik.
| Risiko in Tests | Beispiel-Property | Warum ist das gerade in Tests wichtig? |
|---|---|---|
| Schreiben von Dateien „an die falsche Stelle“ | contenthub.storage.root | Tests können Anhänge und temporäre Dateien erstellen. Wenn der Pfad zufällig oder produktiv ist, verschmutzen Sie das System und erleben seltsame Abstürze. |
| Zufälliger echter HTTP-Aufruf | contenthub.moderation.base-url | Selbst wenn die Integration in Tests gestubt ist, sollte die Adresse sicher und lokal sein, um nicht auf einen echten Dienst zu „schießen“. |
| Nebeneffekte „wie in der Produktion“ | contenthub.notifications.enabled | In Tests ist es praktisch, solche Dinge standardmäßig auszuschalten, damit Tests ihr Ziel prüfen und nicht das „gesamte Anwendungsleben“. |
Genau deshalb sieht die minimale „Start-application-test.yml für Tag 7 so aus und ist dem „Sicherheitspass“ der Testumgebung sehr ähnlich:
# src/test/resources/application-test.yml
contenthub:
storage:
root: "build/test-storage" # Alles innerhalb von build/, um nicht „ins System“ zu schreiben
moderation:
base-url: "http://localhost:8089" # Lokale Adresse, um zufällige externe Aufrufe zu vermeiden
notifications:
enabled: false # Deaktivieren von Nebeneffekten standardmäßig
Ja, hier sind es nur drei Gruppen. Aber das ist in Ordnung. Eine gute Konfiguration beginnt oft klein: Sie fügen nur das hinzu, was die Vorhersagbarkeit und Sicherheit der Tests wirklich beeinflusst.
5. Explizite Aktivierung des Profils: @ActiveProfiles("test")
Selbst wenn Sie eine ideale application-test.yml geschrieben haben, garantiert sie allein nichts. Die Datei wird nur geladen, wenn das Profil test aktiviert ist. Ob es aktiv ist oder nicht, ist genau der Teil, bei dem Anfänger am häufigsten in die Falle „Aber bei mir auf dem Rechner hat es funktioniert“ tappen. Um nicht von zufälligen Umgebungseinstellungen abhängig zu sein, muss das Profil explizit aktiviert werden.
Für Spring-Tests geschieht das mit der Annotation @ActiveProfiles. Sie bezieht sich nicht auf den Produktionscode, sondern auf den Test-Kontext von Spring Test. Sie ändern also nicht das Verhalten der gesamten Anwendung, sondern sagen nur: „Wenn du diese Testklasse ausführst, aktiviere das Profil test.“
Der einfachste „Überprüfungs“-Test im Stil von Tag 7 könnte so aussehen:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest // Kontext hochfahren, damit Profile und Properties wirklich angewendet werden
@ActiveProfiles("test") // Explizites Aktivieren des Test-Profils für diese Testklasse
class StorageRootProfileTest {
@Value("${contenthub.storage.root}") // Liest die Property aus der Environment (in Tests aus application-test.yml)
String storageRoot;
@Test
void readsValueFromTestProfile() {
// Sicherung: Wenn das Profil nicht angewendet wurde, ist der Wert nicht der erwartete, und der Test zeigt das sofort
assertThat(storageRoot).isEqualTo("build/test-storage");
}
}
Hier steckt ein wichtiger pädagogischer Gedanke: Wir machen keinen „nützlichen Geschäftstest“, sondern einen Sicherungstest, der beweist, dass die Konfiguration wirklich übernommen wird. Ein solcher Test hilft oft am Anfang eines Projekts oder bei Migrationen: Sie sehen sofort, dass das Test-Profil lebt und nicht nur in Ihrer Vorstellung existiert.
Und ja, @SpringBootTest ist eine teure Annotation. Wir verwenden sie hier nicht als idealen Stil für die gesamte Suite, sondern als den offensichtlichsten Weg, den Kontext zu starten und zu zeigen, dass das Profil funktioniert. Im echten Leben werden Sie den Kontext genau in dem erforderlichen Umfang starten, aber das ist bereits eine Frage der Wahl des Testtyps; jetzt lösen wir das Problem „Die Konfiguration darf nicht zufällig sein“.
6. Überprüfung des aktiven Profils über Environment
Wenn Sie gerade anfangen, ist es sehr leicht, zwei Gefühle zu verwechseln: „Ich habe die Datei geschrieben“ und „Spring hat die Datei wirklich angewendet“. Daher ist es nützlich, die aktiven Profile über Environment überprüfen zu können. Das ist keine Magie, sondern die übliche Spring-API, die den endgültigen Satz von Einstellungen und Informationen darüber speichert, welche Profile aktiviert sind.
Unten ein Beispiel eines Tests, der genau die Tatsache des aktiven Profils prüft. Er ist nicht von einem bestimmten Schlüssel abhängig und dient als gute „diagnostische Taschenlampe“, wenn etwas schiefgeht:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
import org.springframework.test.context.ActiveProfiles;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest // Spring-Kontext benötigt, damit Environment real ist, nicht „gemalt“
@ActiveProfiles("test") // Sagt Spring Test: Starte diese Klasse im Modus test
class ActiveProfilesSmokeTest {
@Autowired
Environment environment; // Enthält die aktiven Profile und die endgültigen Properties (nach Überlagerungen)
@Test
void testProfileIsActive() {
// Prüft genau die Tatsache: Profil ist aktiviert. Praktisch bei der Diagnose.
assertThat(environment.getActiveProfiles()).contains("test");
}
}
Warum ist das nötig, wenn es bereits einen Test gibt, der contenthub.storage.root liest? Weil das Problem manchmal nicht in einer bestimmten Property liegt, sondern darin, dass das Profil überhaupt nicht aktiviert wurde. Zum Beispiel haben Sie die Annotation vergessen, sie an die falsche Klasse gesetzt oder führen Tests auf eine besondere Art aus, die andere Einstellungen unterschiebt. In einer solchen Situation liefert die Prüfung von getActiveProfiles() ein direkteres Signal: Profil ist aktiviert oder nicht.
Ein zusätzlicher Vorteil dieses Ansatzes ist, dass er hilft, die Tests „ehrlich“ zu halten. Wenn ein Test fehlschlägt, sehen Sie die Ursache nicht als „warum ist der String nicht gleich einem anderen String“, sondern als „warum ist die Umgebung nicht die richtige“. Und das führt normalerweise schneller zu einer Lösung und verringert den Drang, an den Annotationen herumzudoktern.
7. Typische Fehler: application-test.yml
Fehler Nr. 1: Tests hängen von application-test.yml ab, aber das Profil ist nicht explizit aktiviert.
Das ist ein Klassiker: Sie haben application-test.yml geschrieben, sind überzeugt, dass es „standardmäßig für Tests“ gilt, und beginnen, Werte zu verwenden. Dann stellt sich heraus, dass das Profil nicht aktiviert wurde und die Tests die Basiseinstellungen oder irgendwelche Werte aus der Maschinenumgebung übernommen haben. Die Lösung ist sehr einfach: Wenn eine Testklasse wirklich vom Test-Profil abhängt, sollte @ActiveProfiles("test") daneben stehen, damit die Ursache direkt aus dem Code ersichtlich ist.
Fehler Nr. 2: Kopieren der gesamten application.yml in application-test.yml.
Zuerst scheint es so „sicherer“: Die Tests haben ihre eigene vollständige Konfiguration. In der Praxis führt das schnell zu einer Verdoppelung der Realität: Sie ändern eine Basiseinstellung in application.yml, vergessen, die Testkopie zu aktualisieren, und die Tests leben in einem anderen Universum. Viel stabiler ist es, application-test.yml als Schicht von Unterschieden zu halten. Dann wird jeder Schlüssel, der in die Testdatei gelangt, automatisch zu einem Signal: „Das ist wichtig für die Testumgebung“.
Fehler Nr. 3: Verwendung unsicherer Pfade und Adressen im Testprofil.
Manchmal hinterlässt man in Tests einen Pfad wie /tmp/contenthub (der nicht überall gleich existiert) oder, schlimmer, einen Pfad aus den „Produktionseinstellungen“. Dasselbe gilt für URLs: Wenn in der Testkonfiguration versehentlich eine externe Adresse landet, reicht eine ungünstige Bean-Konfiguration, und der Test versucht, ins Netz zu gehen. Ein gutes Testprofil klingt langweilig und lokal: build/test-storage, http://localhost:..., „alles ausgeschaltet außer dem Notwendigen“.
Fehler Nr. 4: application-test.yml wurde in src/main/resources abgelegt.
Technisch kann das funktionieren, aber methodisch ist es eine schlechte Angewohnheit: Die Testkonfiguration beginnt neben der Produktionskonfiguration zu leben und landet leicht im endgültigen Jar. Selbst wenn es den Start nicht zerstört, verwischt es die Grenzen: Es ist unklar, wo die Laufzeitkonfiguration endet und die Testkonfiguration beginnt. Wenn die Datei nur für Tests benötigt wird, sollte sie logischerweise in src/test/resources liegen – dann ist sie für Tests sichtbar, wird aber nicht Teil der Anwendungsauslieferung.
Fehler Nr. 5: Erwartung, dass das Test-Profil automatisch immer und überall aktiviert ist.
Manchmal werden Tests in einer Umgebung so ausgeführt, dass das Profil zufällig eingeschaltet ist (z. B. hat die IDE einige Starteinstellungen übernommen), in einer anderen jedoch nicht. Deshalb sollte man @ActiveProfiles("test") nicht als „überflüssige Annotation“ betrachten, sondern als Teil der Testdokumentation: Sie sagt dem Leser, in welchem Modus dieser Test leben soll. Je weniger „Implizitheit“, desto weniger Überraschungen und desto vorhersagbarer wird die gesamte Test-Suite.
GO TO FULL VERSION