Testcontainers ist in vielen Java-Projekten zu einem unverzichtbaren Werkzeug geworden, um Integrations- und Systemtests in realistischen Umgebungen durchzuführen. Wenn Testszenarien jedoch komplexer werden, stoßen Entwickler oft an Grenzen – insbesondere in Bezug auf Konfiguration, Parallelisierung und Ressourcennutzung. Ein auf Testcontainers aufbauendes Open-Source-Framework begegnet einigen dieser Herausforderungen mit leistungsstarken Erweiterungen.
Testcontainers Infrastructure (TCI) Framework
1. Verbesserte Anpassbarkeit und Parallelisierung
Durch die Verwendung des Factory Patterns bei der Erstellung von Container wird es für den Entwickler einfacher, die Container nach Bedarf anzupassen:
| Ohne TCI | Mit TCI |
static final MySQLContainer MY_SQL_CONTAINER; static { MY_SQL_CONTAINER = new MySQLContainer(); MY_SQL_CONTAINER.start(); } | static final DBTCIFactory DB_INFRA_FACTORY = new DBTCIFactory(); void startInfra() { this.dbInfra = DB_INFRA_FACTORY.getNew(...); } |
Für jede “Infrastruktur” (Abgekürzt: TCI) gibt es eine Factory, um eine neue TCI zu erstellen. Diese Factory kann leicht konfiguriert werden und kümmert sich um Dinge wie die Erstellung von Containern, PreStarting und die Verfolgung der gestarteten TCI für die weiter unten genannten Features.
Container/Infrastruktur kann wie folgt in einer TCI_FACTORY-Klasse angepasst werden:
@Override
public void start(final String containerName) {
super.start(containerName);
if(doMigrate) {
this.migrateDatabase(BASELINE_FOR_TESTS);
}
}
void migrateDatabase(String version) {
// Datenbank mithilfe von z.B. Flyway migrieren
}
Dies bedeutet, dass es nun möglich ist, zusätzlichen, nicht containerbezogenen Code hinzuzufügen, wie z.B. Clients oder allgemeine Methoden (z.B. createUser), ohne den Container selbst zu verändern. Dies folgt dem composition over inheritance Designprinizip.
Durch den Einsatz von Factories kann das Framework auch die Leistung mittels Parallelisierung und PreStarting verbessern.
2. Test so schnell wie möglich ausführen
Warum ist das überhaupt wichtig?
Die schnellstmögliche Ausführung von Tests hat mehrere Vorteile:
- Bei Lokaler Ausführung:
In der Regel gibt es nichts anderes zu tun, wenn man Tests lokal durchführt – außer vielleicht einen Kaffee zu trinken. Es ist auch möglich, eine andere Aufgabe zu beginnen, aber dann verliert man vielleicht den Fokus auf die ursprüngliche Aufgabe und muss sich später wieder in das Thema einarbeiten.
- Bei Benutzung einer CI:
- Wenn man für Rechenleistung bei Bedarf bezahlt (z. B. minutenbasierte Abrechnung von Spot-Instances) kann die schnellere Durchführung von Tests (ohne Vergrößerung des verwendeten Rechners) aufgrund der geringeren Mietdauer viel Geld sparen.
- Wenn man für eine feste Menge an Rechenleistung bezahlt, bedeutet eine schnellere Ausführung von Tests, dass mehr Zeit für andere Aufgaben zur Verfügung steht, die auf der CI ausgeführt werden können.
Wenn die Zeitersparnis groß genug ist, kann man auch über eine Verringerung der erforderlichen Rechenleistung nachdenken.
- Schnelleres Test-Feedback: Wenn z.B. vor einem Release alle Integrationstests erfolgreich ausgeführt werden müssen, kann dies die Zeit für die Auslieferung des Releases verkürzen.
- Wenn man für Rechenleistung bei Bedarf bezahlt (z. B. minutenbasierte Abrechnung von Spot-Instances) kann die schnellere Durchführung von Tests (ohne Vergrößerung des verwendeten Rechners) aufgrund der geringeren Mietdauer viel Geld sparen.
Das Framework ist explizit auf Parallelisierung ausgelegt und bietet mehrere Funktionen zur Beschleunigung von Tests:
2.1. PreStarting Mechanismus
Bei der Durchführung von Tests gibt es in der Regel bestimmte Zeiten, in denen die verfügbaren Ressourcen kaum ausgelastet sind:

PreStarting verwendet einen gecachten Pool von Infrastrukturen und versucht, diese Leerlaufzeiten zu nutzen, um diesen Pool aufzufüllen. Wenn eine neue Infrastruktur angefordert wird, muss nicht auf ihre Erstellung gewartet werden, sondern es kann die bereits gestartete Infrastruktur aus diesem Pool verwendet werden – sofern sie verfügbar ist.
Performance-Boost
Wenn PreStarting korrekt implementiert wurde, kann dies einen enormen Leistungsunterschied bewirken, wie in diesem Leistungsvergleich zu sehen ist
Des Weiteren gibt es auch ein Live-Beispiel (mit GitHub Actions), das die folgenden Ergebnisse liefert:
| Fall | Parallelisierung | PreStarting aktiv? | Benötigte Zeit um alle Tests auszuführen |
|---|---|---|---|
| A | – | ❌ | 8m 50s |
| B | – | ✔ | 5m 30s |
| C | 2 | ❌ | 6m |
| D | 2 | ✔ | 4m 50s |
Wie oben gezeigt, erzielt die beste Konfiguration (D) eine Geschwindigkeitssteigerung von fast 50 % im Vergleich zur Baseline (A).
2.2. Optimierte Testcontainers Netzwerke
Es wird eine optimierte Implementierung von Testcontainers Network verwendet:
| Vorher | Nachher |
| NetworkImpl code aus Testcontainers 1.20 – siehe unten. | LazyNetworkPool bietet einen Pool von Netzwerken, die im Hintergrund erstellt werden. Es geht keine Zeit mit dem Warten auf die Netzwerkerstellung verloren. |
@Override
public synchronized String getId() {
if (initialized.compareAndSet(false, true)) {
boolean success = false;
try {
// Netzwerk wird erstellt wenn auf die ID zugegriffen wird
// Dauert einen Moment
id = create();
success = true;
} finally {
...
}
}
return id;
}
2.3. Container Leck Erkennung
Erkennt ob gestartete Container/Infrastruktur auch beendet wurden und verhindert dadurch, dass es zur Ressourcenerschöpfung kommt.
Im folgenden Beispiel wird der Testcontainer zwar erstellt, aber nie beendet.
@Test
void test() {
DummyTCI tci = DUMMY_FACTORY.getNew(...);
...
}
Nachdem die Tests mit dem Framework ausgeführt wurden, erscheint folgende Fehlermeldung:
ERROR s.x.tci.leakdetection.TCILeakAgent - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ERROR s.x.tci.leakdetection.TCILeakAgent - ! PANIC: DETECTED CONTAINER INFRASTRUCTURE LEAK !
ERROR s.x.tci.leakdetection.TCILeakAgent - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
ERROR s.x.tci.leakdetection.TCILeakAgent - All test are finished but some infrastructure is still marked as in use:
DummyTCIFactory leaked 1x [container-ids=[c1b6be852fac3bf65ac8f2739ab161d7f95bc4c62699c698ccc8b74da1be8a3d]]
3. Quality of Life Verbesserungen
Das Framework enthält auch einige kleinere Verbesserungen:
3.1. Container-Namen die ein Mensch lesen kann
Alle gestarteten Container haben einen eindeutigen, von Menschen lesbaren Namen, was die Identifizierung erleichtert.
| Vorher | Nachher |
docker statsNAMEeager_rubin vigilant_archimedespractical_haibtecstatic_sandersonserene_einsteingreat_saha agitated_dhawan strange_montalcini | docker stats |
3.2. Statistiken zur Testlaufzeit
Ein Nachverfolgungsmechanismus, der das Auffinden von Engpässen und ähnlichen Problemen erleichtert.
Beispiel:
[main] [i.tracing.TCITracingAgent] === Test Tracing Info ===
Duration: 2m 43.608s
Tests: 20.656s / 15 / 5m 9.84s
BrowserTCIFactory-firefox:
bootNew - 1ms / 6 / 5ms
connectToNetwork - 515ms / 5 / 2.575s
getNew - 574ms / 5 / 2.87s
infraStart(async) - 14.575s / 6 / 1m 27.448s
postProcessNew - 54ms / 5 / 270ms
warmUp - 2.448s / 1 / 2.448s
...
Weiterführende Infos
Das Framework ist auf GitHub verfügbar. Der Usage-Bereich enthält eine Einführung. Es stehen auch Demo-Projekte bereit.
Fragen oder Anregungen können über den GitHub Issue Tracker eingereicht werden.