1. Kontrollierte Umgebung des Testkontexts
Wenn man Tests als Lebewesen betrachtet (ja, ich rede manchmal mit Tests – das ist eine berufliche Deformation), dann lieben sie zwei Dinge: Vorhersagbarkeit und Ruhe. Vorhersagbarkeit bedeutet, dass IDs nicht zufällig sind, Ausgabepfade nicht vom Computer abhängen und das Verhalten des „Anwendungsmodus“ sich nicht ändert, weil Sie gestern dev gestartet haben und heute demo. Ruhe bedeutet, dass der Test keine Gigabytes in die Konsole schreibt und keine Dateien im Projektwurzelverzeichnis erstellt, die „archäologische Schichten“ hinterlassen.
Der Smoke-Test hat bereits gezeigt, dass der Kontext überhaupt startet und das Wiring nicht kaputt ist. Jetzt stellt sich eine bodenständigere Frage: Wie bringt man diesen Kontext dazu, immer im gleichen test-Modus zu starten, und nicht in einer zufälligen Mischung aus Profilen, alten Eigenschaften und lauten Implementierungen?
Unsere ContextFlow ist eine Nicht-Web-Anwendung ohne Datenbank, aber sie lebt dennoch in einer Umgebung: Profile (dev/demo/test), Properties (contextflow.*), Auswahl der Implementierungen (ConsoleAuditWriter vs. FileAuditWriter, UuidOrderIdGenerator vs. DeterministicOrderIdGenerator). Im normalen Start ist das großartig: Sie ändern die Umgebung – das Verhalten ändert sich. In Tests kann dies zur Quelle von Chaos werden: Sie erwarteten, dass der Test eine Bestellung mit einer vorhersagbaren ID erstellt, aber plötzlich generierte er eine UUID; Sie erwarteten, dass Berichte in build/ geschrieben werden, aber sie landeten unerwartet in einem „alten“ Pfad aus einem vorherigen Lauf.
Daher lernen wir heute, wie man die Umgebung fest und dennoch vorsichtig an den Test „anschraubt“. Dazu haben wir drei Hauptwerkzeuge: @ActiveProfiles, @TestPropertySource und test-spezifische @Configuration (mit Stub-Beans). Es ist wichtig zu verstehen: Das ist keine „Magie um der Magie willen“, sondern eine Möglichkeit, Testbedingungen zu einem Teil des Testvertrags zu machen. Das bedeutet, dass jeder (einschließlich Sie selbst in zwei Wochen) den Test öffnet und sofort sieht: welches Profil aktiv ist, welche Eigenschaften überschrieben wurden und welche Abhängigkeiten ersetzt wurden.
2. @ActiveProfiles: Profile für Tests
Profile in Spring haben wir bereits früher besprochen: Es ist eine Möglichkeit, verschiedene Varianten derselben Anwendung ohne if-else im Geschäftslogikcode zu bündeln. In Tests werden Profile noch wichtiger, denn Tests sind nicht „ein weiterer Start der Anwendung“, sondern ein separater Lebensmodus, in dem man meistens Determinismus, minimale Nebeneffekte und schnelle Überprüfungen wünscht.
Die Annotation @ActiveProfiles("test") macht eine einfache Sache: Sie teilt Spring Test mit, welche aktiven Profile beim Start des Testkontexts in der Environment gesetzt werden sollen. Danach wird der Container so zusammengebaut, als ob die Anwendung im Profil test gestartet wäre: @Profile("test")-Beans werden geladen, @Profile("dev") und @Profile("demo") werden ausgeschlossen, und Ihre gesamte profilbewusste Konfiguration beginnt für den Test zu arbeiten.
Damit das keine Abstraktion bleibt, ist es hilfreich, eine „Karte“ unserer Profile speziell für ContextFlow im Kopf zu haben. Dies ist keine Liste „aller möglichen“, sondern genau die Punkte, die für ein Übungsprojekt normalerweise wichtig sind:
| Komponente / Port | dev (lokal, einfach) | demo (Demonstration) | test (deterministisch) |
|---|---|---|---|
| OrderIdGenerator | UUID (realistisch, aber zufällig) | kann UUID oder fest sein | deterministischer Generator |
| NotificationSender | konsolenbasiert | konsolenbasiert/„demonstrativ“ | NoOp oder Stub |
| AuditWriter | konsolenbasiert | dateibasiert in build/ | in build/ oder In-Memory/No-Op |
| Berichtsausgabepfade | build/… | build/… | build/test-… (um keine Vermischung zu verursachen) |
Der Hauptgedanke: Der Test braucht oft das Profil test, auch wenn Sie im „echten Start“ häufiger dev verwenden.
Ein minimaler Test, der prüft, ob das Profil tatsächlich das Wiring beeinflusst hat, könnte so aussehen (beachten Sie: Wir prüfen nicht das „Geschäftsergebnis“, sondern genau die Bauvariante):
import com.example.contextflow.domain.ports.OrderIdGenerator;
import com.example.contextflow.infrastructure.id.DeterministicOrderIdGenerator;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertTrue;
// Heben den Spring-Kontext nur aus der angegebenen Konfiguration (ohne die gesamte Anwendung zu starten)
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
// Fixieren das Profil explizit, um nicht von IDE/Startparametern abzuhängen
@ActiveProfiles("test")
class TestProfileWiringTest {
// Injizieren den Port: Uns interessiert nicht die konkrete Implementierung namentlich, sondern was der Container gewählt hat
@Autowired
OrderIdGenerator orderIdGenerator;
@Test
void usesDeterministicGeneratorInTestProfile() {
// Prüfen genau die "Bauvariante": Im test-Profil sollte ein deterministischer Generator sein
assertTrue(orderIdGenerator instanceof DeterministicOrderIdGenerator);
}
}
Hier gibt es eine wichtige Nuance aus dem „echten Spring-Leben“, die man sich bereits jetzt merken sollte. Wenn auf der Service-Ebene AOP aktiviert ist (und in unserem Projekt ist das der Fall: ServiceTimingAspect), können einige Beans proxied sein, und eine Prüfung wie bean.getClass() == SomeClass.class könnte unerwartet fehlschlagen. Daher ist für die Frage „was ist es eigentlich“ eher instanceof oder eine Prüfung über das Interface geeignet, wie oben.
Und noch eine Nuance: @ActiveProfiles im Test ist kein „Feature der Schönheit“. Es ist eine Möglichkeit, sich davor zu schützen, dass der Test versehentlich mit dem Standardprofil startet. In der Folge erhalten Sie statt NoOpNotificationSender einen konsolenbasierten Sender, und der Test beginnt die Ausgabe zu verschmutzen. Er wird zwar nicht logisch scheitern, aber er wird zu einem lauten Test, und laute Tests überleben nicht lange: Sie werden ignoriert.
3. @TestPropertySource: Überschreibungen von Eigenschaften
Profile lösen die Frage „welche Beans nehmen überhaupt an der Zusammenstellung teil“. Aber oft reicht das nicht: Selbst im test-Profil müssen Sie punktuell Einstellungen anpassen. Zum Beispiel möchten Sie, dass Berichte in build/test-reports geschrieben werden und nicht in das gemeinsame build/reports, damit Tests nicht mit Ihren manuellen Starts vermischt werden. Oder Sie möchten irgendein Limit reduzieren, damit der Test schneller durchläuft (in unserem Projekt gibt es nicht viele Limits, aber in realen Anwendungen ist das klassisch).
Dafür gibt es @TestPropertySource. Es fügt dem Environment gezielt für den Testkontext Property-Quellen hinzu. Das Bequemste, besonders für Anfänger, sind Inline-Überschreibungen über properties = .... Sie werden direkt im Test gelesen, benötigen keine separate Datei und zeigen die Absicht sehr deutlich.
Hier ein minimales Beispiel: Wir testen nicht die „Berichtserstellung“, sondern zeigen lediglich, dass die Eigenschaft in der Testumgebung überschrieben wurde:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;
// Heben den Kontext aus der "produktiven" Konfiguration, fügen aber eine Test-Property-Schicht hinzu
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
// Inline-Überschreibung: wird direkt im Test gelesen und zeigt die Absicht deutlich
@TestPropertySource(properties = "contextflow.report.output-dir=build/test-reports")
class PropertyOverrideTest {
// Verwenden Environment als ehrlichste Methode, die endgültigen Properties zu sehen
@Autowired
Environment environment;
@Test
void overridesReportOutputDir() {
// Prüfen nicht das Verhalten des Beans, sondern den endgültigen Wert der Property in der Umgebung
String dir = environment.getProperty("contextflow.report.output-dir");
assertEquals("build/test-reports", dir);
}
}
Beachten Sie die „Erwartungsebene“ in diesem Test. Wir prüfen nicht, ob das Verzeichnis tatsächlich erstellt wurde und ob dort eine Datei geschrieben wurde. Das ist bereits eine szenariobasierte Verhaltensprüfung (und wir überlassen sie bewusst der nächsten Vorlesung). Hier fixieren wir nur die Tatsache: Im Testkontext hat die Eigenschaft den Wert so-und-so. Das ist sehr nützlich als schnelle Diagnose: Wenn später Tests nicht dorthin schreiben, wo sie sollen, erkennen Sie schnell, dass auf der Property-Ebene etwas kaputt ist.
Neben Inline-Überschreibungen kann @TestPropertySource auch eine Test-Property-Datei einbinden. Das ist praktisch, wenn Sie viele Tests haben und denselben String nicht in jede Klasse kopieren möchten. Zum Beispiel können Sie src/test/resources/contextflow-test-overrides.properties erstellen und so einbinden:
import org.junit.jupiter.api.Test;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
// Binden eine separate Datei mit Test-Überschreibungen ein (liegt in src/test/resources)
@TestPropertySource(locations = "/contextflow-test-overrides.properties")
class PropertyFileOverrideTest {
@Test
void contextStartsWithExtraTestProperties() {
// Smoke-Prüfung: Datei gefunden und Kontext startet mit zusätzlichen Eigenschaften
assertTrue(true);
}
}
Dieser Test sieht „leer“ aus, und das ist als Lehrbeispiel normal: Er demonstriert den Mechanismus. In einem realen Projekt würden Sie entweder eine bestimmte Eigenschaft prüfen oder diese Property-Datei als Basis für Szenariotests verwenden. Aber selbst ein „leerer“ Test ist manchmal nützlich als Smoke-Test auf das Vorhandensein der Datei und die Korrektheit der Pfade.
Prioritäten von Eigenschaften
Eigenschaften in Spring sind wie Kleidungsschichten: Je näher am Körper, desto schwerer zu ignorieren. Aber für diese Vorlesung ist es nützlicher, nicht den gesamten globalen Precedence-Graphen von Spring auswendig zu lernen, sondern drei Arbeitsregeln im Kopf zu behalten.
- Normale Anwendungseigenschaften, einschließlich profil-spezifischer Werte, geben die Basislinie vor.
- @TestPropertySource(locations = ...) legt eine testdateibasierte Schicht darüber.
- @TestPropertySource(properties = ...) überschreibt Werte aus der dateibasierten Testquelle und eignet sich für punktuelle Überschreibungen direkt beim Test.
Daher sind Inline-Überschreibungen so praktisch: Die lokalste und lesbarste Testschicht lebt direkt im Testcode.
flowchart TD
A["Normale Anwendungseigenschaften
(einschließlich profil-spezifischer)"] --> B["@TestPropertySource(locations = ...)"]
B --> C["@TestPropertySource(properties = ...)"]
Systemeigenschaften und Umgebungsvariablen können ebenfalls an der Eigenschaftsauflösung teilnehmen, aber hier machen wir sie bewusst nicht zum alltäglichen Hebel. Sobald die Teststeuerung in die externe Umgebung abwandert, wird die Diagnose schnell trübe: Der Wert kommt scheinbar „von irgendwoher“, aber neben dem Test ist das nicht mehr sichtbar.
4. Testspezifische @Configuration: Test-Beans
Manchmal reichen Profile und Eigenschaften immer noch nicht. Der Grund ist einfach: Es gibt Abhängigkeiten, die in der „echten“ Anwendung Nebeneffekte haben, aber in Tests nur stören. Zum Beispiel kann NotificationSender in die Konsole schreiben (und Sie sehen Müll in der Testausgabe). Oder AuditWriter kann in eine Datei schreiben. Oder ein infrastruktureller Bean liest Ressourcen, und Sie möchten ihn im Test durch eine vereinfachte Version ersetzen.
In der Boot-Welt zeigt man oft @MockBean. Aber wir sind im Kurs Spring Core, und unser Ziel ist es, den Container zu verstehen, nicht das Wiring hinter einer „magischen Annotation“ zu verstecken. Daher machen wir es ehrlicher: Wir fügen eine zusätzliche Testkonfiguration hinzu und registrieren darin eine Stub- oder No-Op-Implementierung.
Üblicherweise sieht das so aus: Im Test gibt es eine kleine @Configuration-Klasse (oft als static-Innere Klasse), die Test-Beans deklariert. Dann startet der Test den Kontext aus zwei Konfigurationen: der Produktions-ContextFlowAppConfig und der Test-Überschreibungskonfiguration.
Beispiel: Wir ersetzen NotificationSender durch „Stille“ – No-Op. Und damit der Container ihn beim Autowiring auswählt, markieren wir ihn mit @Primary.
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
class NoOpNotificationTestConfig {
@Bean
@Primary // In Tests soll dieser Bean alle Produktionsimplementierungen "überschreiben"
NotificationSender notificationSender() {
// No-Op-Implementierung: entfernt Nebeneffekte (Konsole/Dateien/Netzwerk) aus Tests
return message -> { /* absichtlich keine Operation */ };
}
}
Es sieht lustig aus, aber das ist eine nützliche „pädagogische Einfachheit“. Sie haben einen Port NotificationSender, und in Tests geben Sie ihm eine Implementierung, die nichts tut. Tests werden leiser und vor allem vorhersagbarer: keine „zufälligen“ Dateien, keine Konsole, keine Netzwerkaufrufe und andere Überraschungen.
Diese Konfiguration wird so eingebunden:
import com.example.contextflow.domain.ports.NotificationSender;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@SpringJUnitConfig(classes = {
com.example.contextflow.config.core.ContextFlowAppConfig.class,
NoOpNotificationTestConfig.class // Mischen die Testkonfiguration mit Ersetzungen bei
})
class TestSpecificConfigWiringTest {
// Injizieren den Port: wollen sicherstellen, dass der Kontext zusammengebaut wurde und der Bean gefunden wird
@Autowired
NotificationSender notificationSender;
@Test
void testContextUsesNoOpNotificationSender() {
// Minimale Prüfung: "Kontext startet und Abhängigkeit existiert"
assertNotNull(notificationSender);
}
}
Dieses Beispiel prüft nicht „dass es genau No-Op ist“, es zeigt das Prinzip: Der Testkontext kann als „Produktion plus Überlagerung“ zusammengebaut werden. In einem echten Test könnten Sie zusätzlich den Typ oder das Verhalten prüfen, aber das Wesentliche ist der Mechanismus selbst. Und ja, hier verwenden wir @Autowired im Test über ein Feld, und das ist in Ordnung: Die Testklasse ist kein Produktionscode, hier ist das Ziel nicht Unveränderlichkeit, sondern bequemer Zugriff auf die Beans des Kontexts. Übertragen Sie diese Gewohnheit nur nicht auf die Services der Anwendung – die nehmen das übel und beginnen ein geheimes Leben.
5. Stub-Beans: Ersetzungen für Kontrollierbarkeit
Stub ist kein schlimmes Wort. Es ist kein „Mock-Framework“, keine Magie, keine Alchemie. Ein Stub ist einfach eine einfache Implementierung eines Interfaces, die sich vorhersagbar verhält und oft hilft, entweder Lärm zu beseitigen oder eine Tatsache eines Aufrufs festzuhalten. In unserem Kurs setzen wir bewusst auf solche einfachen Ersetzungen, weil sie lehren, architektonisch zu denken: „Ich habe einen Port, also kann ich die Implementierung ersetzen“.
Nehmen wir an, wir haben NotificationSender und möchten, dass der Test-Sender nichts ausgibt, aber dennoch erkennen lässt, „welcher Sender im Test tatsächlich verwendet wird“. Machen wir eine Stub-Klasse, die die letzte Nachricht speichert. Das ähnelt bereits einer Vorbereitung auf Verhaltensprüfungen, aber in dieser Vorlesung verwenden wir es genau als Werkzeug der „kontrollierten Umgebung“ und nicht als vollständigen szenariobasierten Test.
import com.example.contextflow.domain.ports.NotificationSender;
class StubNotificationSender implements NotificationSender {
// Speichern die letzte Nachricht, damit der Test "hineinschauen" und die Tatsache des Sendens prüfen kann
private String lastMessage;
@Override
public void send(String message) {
// Keine Nebenwirkungen: nur die Eingabedaten merken
lastMessage = message;
}
String getLastMessage() {
// Praktischer Getter für Tests
return lastMessage;
}
}
Jetzt kann dieser Stub über eine testspezifische Konfiguration registriert werden:
import com.example.contextflow.domain.ports.NotificationSender;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
class StubNotificationConfig {
@Bean
@Primary // Dieser Stub soll der Hauptkandidat für NotificationSender im Testkontext sein
StubNotificationSender notificationSender() {
// Ein einziger Bean reicht: Er kann sowohl als Port als auch als konkreter Stub injiziert werden
return new StubNotificationSender();
}
}
Dieser eine Bean reicht aus. Der Container kann ihn sowohl als NotificationSender als auch als StubNotificationSender ausgeben, weil die konkrete Klasse den benötigten Port implementiert. Eine doppelte Registrierung würde hier nur das Bild verwirren.
Wenn Sie jetzt denken: „Moment, das ähnelt aber schon einer Verhaltensprüfung!“ – haben Sie Recht. Daher beschränken wir uns in Vorlesung 4 darauf, dass der Stub hilft, die Umgebung kontrollierbar zu machen (wir wissen, dass es keine echten Nebenwirkungen gibt), und vollständige Szenarioprüfungen (Bestellung erstellt → Ereignis fliegt → Listener reagiert) heben wir uns für die nächste Vorlesung auf, wo dies das Hauptlehrthema sein wird.
Die Hauptdisziplin für Stubs ist: Sie sollen dumm und vorhersagbar sein. Wenn Sie einen Stub geschrieben haben, der intern JSON parst, Dateien öffnet und noch die Implementierung nach Profil auswählt ... herzlichen Glückwunsch, Sie haben versehentlich eine Mini-Anwendung innerhalb des Tests geschrieben. So etwas endet meistens schlecht.
6. Zusammenbau des Testkontexts ContextFlow
Wenn diese drei Mechanismen zusammenspielen, ergibt sich ein sehr praktischer „Test-Baumodus“ – Sie fixieren an einer Stelle: welches Profil, welche Eigenschaften und welche Abhängigkeiten ersetzt wurden. Dadurch ist der Test unabhängig von Ihrem Rechner und benötigt keine „richtige Stimmung“ der Umgebung.
Lassen Sie uns ein Beispiel einer „kontrollierten“ Testumgebung zusammenbauen, die drei Dinge gleichzeitig tut: das test-Profil aktiviert, den Berichtsausgabepfad überschreibt und Benachrichtigungen über Stub/No-Op deaktiviert.
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringJUnitConfig(classes = {
com.example.contextflow.config.core.ContextFlowAppConfig.class,
NoOpNotificationTestConfig.class // Ersetzung, damit Tests "leise" sind
})
@ActiveProfiles("test") // Fixieren das test-Profil: Determinismus und minimale Nebenwirkungen
@TestPropertySource(properties = "contextflow.report.output-dir=build/test-reports") // Schreiben Berichte in einen separaten Ordner
class ManagedEnvironmentSmokeTest {
@Test
void contextStartsInPredictableTestMode() {
// Smoke: Umgebungsvorlage wird korrekt zusammengebaut
assertTrue(true);
}
}
Schon wieder ein „leerer“ Test? Ja. Und das ist im Lehrformat normal: Er zeigt die „Vorlage“ für die meisten Kontexttests. Sobald Sie anfangen, echte Prüfungen zu schreiben, werden Sie @Autowired für die benötigten Beans hinzufügen und Assertions darauf machen. Aber die Umgebungsvorlage bleibt dieselbe: Profil + Eigenschaften + Test-Beans.
Wenn Sie diesen Test etwas aussagekräftiger machen möchten, ohne jedoch in ein vollständiges Szenario abzudriften, können Sie zwei sanfte Prüfungen hinzufügen: dass das Profil aktiv ist und dass die Property-Überschreibung angewendet wurde. Das ist eine Umgebungsprüfung, keine Geschäftslogik:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringJUnitConfig(classes = com.example.contextflow.config.core.ContextFlowAppConfig.class)
@ActiveProfiles("test") // Wollen den Test-Baumodus
@TestPropertySource(properties = "contextflow.report.output-dir=build/test-reports") // Und einen separaten output-dir für Berichte
class EnvironmentSanityTest {
// Prüfen die Umgebung über Environment: Das ist die "Wahrheit" bezüglich Profile und Properties
@Autowired
Environment environment;
@Test
void profileAndPropertyAreApplied() {
// Profil ist tatsächlich aktiv
assertTrue(environment.acceptsProfiles("test"));
// Und die Eigenschaft wurde wirklich überschrieben
assertEquals("build/test-reports",
environment.getProperty("contextflow.report.output-dir"));
}
}
Solche Prüfungen werden oft zu einem „Sicherheitsnetz“. Wenn Sie in einem Monat die Konfiguration ändern und versehentlich das Laden der Property-Quellen beschädigen, wird dieser Test schnell und verständlich fehlschlagen: nicht tief im Geschäftsszenario, sondern direkt auf der Ebene „Umgebung ist nicht die richtige“.
7. Typische Fehler bei der Verwaltung der Testumgebung
Wenn man beginnt, @ActiveProfiles, @TestPropertySource und Stub-Beans aktiv zu nutzen, kommt fast immer der Moment „Warum funktioniert es nicht, ich habe die Annotation doch genau geschrieben“. Das ist eine normale Entwicklungsphase, etwa wie „Warum hat git rebase alles kaputt gemacht?“ – jeder, der in der realen Welt lebt, macht das durch. Wichtig ist, keine Zaubersprüche zu lernen, sondern die Ursachen zu verstehen.
Fehler Nr. 1: Tests starten nicht im test-Profil, sondern „wie es kommt“.
Wenn Sie sich auf das „Standardprofil“ oder darauf verlassen, dass die IDE „irgendwie spring.profiles.active setzt“, erhalten Sie mit ziemlicher Sicherheit Unvorhersagbarkeit. Heute startet der Test in dev, morgen ganz ohne Profil, und übermorgen aktiviert CI einen anderen Satz. Der einfachste Fix: Setzen Sie explizit @ActiveProfiles("test") dort, wo der Test genau den test-Baumodus benötigt.
Fehler Nr. 2: Eigenschaften wurden überschrieben, aber Sie prüfen die falsche Stelle.
Anfänger verwenden manchmal @TestPropertySource(...) und prüfen dann ein bestimmtes Feld innerhalb eines Beans und wundern sich, dass es „alt“ ist. Oft ist die Ursache banal: Entweder der Bean wird nicht aus dieser Eigenschaft erstellt (anderer Schlüssel) oder der Wert im Bean wird komplexer berechnet als gedacht. Zur Diagnose prüfen Sie zuerst Environment.getProperty(key) – das ist die ehrlichste „Wahrheitsquelle“ für Property-Werte, und erst danach prüfen Sie das Verhalten des Beans.
Fehler Nr. 3: Sie haben eine Eigenschaft überschrieben, verstehen aber nicht, dass eine andere sie überschreibt.
Wenn Sie im Test eine Property-Datei über @TestPropertySource(locations=...) eingebunden haben und dann noch irgendwo ein Inline-@TestPropertySource(properties=...) hinzufügen, vergisst man leicht, wer wen überschreibt. Versuchen Sie, Überschreibungen kompakt und testnah zu halten; wenn es viele Überschreibungen gibt, erstellen Sie eine „gemeinsame“ Datei für das Testpaket und fügen Sie in konkreten Tests nur punktuelle Inline-Änderungen hinzu.
Fehler Nr. 4: Stub-Bean wurde hinzugefügt, aber der Container wählt trotzdem den „produktiven“ Bean.
Das ist klassisch, wenn im Kontext zwei Implementierungen des Interfaces vorhanden sind und das Autowiring die falsche wählt. Meistens haben Sie @Primary auf dem Stub vergessen, oder im Produktionscode gibt es eine Injektion über @Qualifier, die auf einen bestimmten Namen verweist. Hier ist wichtig: Der Stub sollte nicht „ein weiterer Kandidat“ sein, sondern der ausgewählte Kandidat. Das erreichen Sie entweder durch @Primary, oder durch einen abgestimmten @Qualifier, oder dadurch, dass Sie für den Test einen engen Kontext starten, in dem es den „produktiven“ Kandidaten nicht gibt.
Fehler Nr. 5: Test-Überschreibungen beginnen, eine eigene Architektur zu werden.
Wenn es zu viele Testkonfigurationen gibt und sie anfangen, die Hälfte des Produktions-Wirings zu duplizieren, geraten Sie in eine Falle: Die Tests hören auf, die echte Anwendung zu prüfen, und beginnen, die „Testversion der Anwendung“ zu prüfen. Eine gute Faustregel ist: Der Testkontext sollte der Produktion so ähnlich wie möglich sein, aber mit minimalen Ersetzungen für Determinismus und Kontrolle von Nebeneffekten. Wenn Sie die Hälfte der Konfiguration umgeschrieben haben, testen Sie bereits eine andere Welt.
GO TO FULL VERSION