Integrationstests validieren das Zusammenspiel mehrerer Komponenten und externe Abhängigkeiten wie Datenbanken oder Web-Services. Im Gegensatz zu Unit-Tests benötigen sie längere Ausführungszeiten, spezielle Testumgebungen und zusätzliche Dependencies. Die Vermischung mit Unit-Tests führt zu langsamen Feedback-Zyklen und komplexen Test-Setups. Eine dedizierte Integration-Test-Configuration löst diese Probleme durch klare Trennung.
Die Standardkonfiguration von Gradle kennt nur die test
Task für Unit-Tests. Integrationstests in derselben Configuration
unterzubringen bedeutet, dass jeder gradle test Aufruf auch
zeitintensive Integrationstests ausführt. Entwickler umgehen dies oft
durch @Disabled Annotations oder Test-Categories, was zu
inkonsistenter Test-Ausführung führt. Eine separate Configuration
ermöglicht gezieltes Ausführen: gradle test für schnelles
Feedback, gradle integrationTest für vollständige
Validierung.
Die technische Trennung erfolgt durch eigene Source Sets,
Configurations und Tasks. Integrationstests erhalten ein eigenes
Verzeichnis src/integrationTest, separate Dependencies und
eine unabhängige Test-Task. Diese Struktur spiegelt die
unterschiedlichen Anforderungen wider und macht den Build-Prozess
transparenter.
Der erste Schritt ist die Definition eines neuen Source Sets für Integrationstests. Dies erstellt die notwendige Verzeichnisstruktur und Configurations:
sourceSets {
create("integrationTest") {
java.srcDir("src/integrationTest/java")
resources.srcDir("src/integrationTest/resources")
compileClasspath += sourceSets.main.get().output + configurations.testCompileClasspath.get()
runtimeClasspath += output + compileClasspath
}
}Diese Konfiguration etabliert src/integrationTest/java
für Testcode und src/integrationTest/resources für
Testressourcen wie Konfigurationsdateien oder SQL-Skripte. Der
Compile-Classpath inkludiert den kompilierten Hauptcode und alle
Test-Dependencies. Der Runtime-Classpath fügt die kompilierten
Integrationstests hinzu.
Die Verzeichnisse müssen manuell erstellt werden, da Gradle sie nicht automatisch generiert. Die Struktur folgt der Konvention anderer Source Sets:
src/
├── main/
│ ├── java/
│ └── resources/
├── test/
│ ├── java/
│ └── resources/
└── integrationTest/
├── java/
└── resources/
IDEs wie IntelliJ IDEA erkennen das neue Source Set nach einem Gradle-Sync automatisch. Die Verzeichnisse werden als Test-Sources markiert und erhalten entsprechende Icons und Syntax-Highlighting. Dies ermöglicht nahtlose Entwicklung von Integrationstests mit voller IDE-Unterstützung.
Die Configurations für Integrationstests müssen korrekt strukturiert werden, um Dependency-Management und Classpath-Resolution zu ermöglichen:
val integrationTestImplementation by configurations.getting {
extendsFrom(configurations.implementation.get())
extendsFrom(configurations.testImplementation.get())
}
val integrationTestRuntimeOnly by configurations.getting {
extendsFrom(configurations.runtimeOnly.get())
extendsFrom(configurations.testRuntimeOnly.get())
}
val integrationTestCompileClasspath by configurations.getting {
extendsFrom(integrationTestImplementation)
}
val integrationTestRuntimeClasspath by configurations.getting {
extendsFrom(integrationTestImplementation)
extendsFrom(integrationTestRuntimeOnly)
}Die integrationTestImplementation Configuration erbt von
implementation und testImplementation. Dadurch
stehen alle Produktiv-Dependencies und Test-Frameworks zur Verfügung.
Zusätzliche Integrationtest-spezifische Dependencies werden direkt zu
dieser Configuration hinzugefügt.
Die Vererbungshierarchie stellt sicher, dass Integrationstests Zugriff auf alle notwendigen Bibliotheken haben. Unit-Test-Utilities wie Mockito oder AssertJ sind automatisch verfügbar. Gleichzeitig bleiben Integrationtest-spezifische Dependencies wie Testcontainers oder REST-Assured isoliert vom normalen Test-Scope.
Integrationstests benötigen oft zusätzliche Dependencies für
Datenbankzugriffe, HTTP-Clients oder Container-Management. Diese werden
zur integrationTestImplementation Configuration
hinzugefügt:
dependencies {
integrationTestImplementation("org.testcontainers:testcontainers:1.19.0")
integrationTestImplementation("org.testcontainers:postgresql:1.19.0")
integrationTestImplementation("io.rest-assured:rest-assured:5.3.2")
integrationTestImplementation("org.springframework.boot:spring-boot-starter-test")
integrationTestRuntimeOnly("org.postgresql:postgresql:42.6.0")
}Testcontainers ermöglicht das Starten von Docker-Containern für
Datenbanken oder andere Services direkt aus Tests. REST Assured
vereinfacht das Testen von REST-APIs mit einer fluent API. Der
PostgreSQL-Driver wird nur zur Laufzeit benötigt und landet daher in
integrationTestRuntimeOnly.
Die Dependency-Verwaltung folgt denselben Prinzipien wie bei normalen Dependencies. Version Catalogs können verwendet werden, um Versionen zentral zu verwalten. Dependency Constraints gelten auch für Integrationtest-Dependencies und sorgen für konsistente Versionen über alle Scopes hinweg.
Ein wichtiger Aspekt ist die Vermeidung von Dependency-Konflikten
zwischen Test- und Integrationtest-Scope. Wenn unterschiedliche
Versionen derselben Bibliothek benötigt werden, müssen explizite
Excludes oder Force-Direktiven verwendet werden. Die
gradle integrationTestDependencies Task zeigt den
vollständigen Dependency-Tree für Debugging-Zwecke.
Die Integrationtest-Task muss explizit definiert und konfiguriert werden:
tasks.register<Test>("integrationTest") {
description = "Runs integration tests"
group = "verification"
testClassesDirs = sourceSets["integrationTest"].output.classesDirs
classpath = sourceSets["integrationTest"].runtimeClasspath
shouldRunAfter(tasks.test)
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
exceptionFormat = TestExceptionFormat.FULL
showStandardStreams = true
}
systemProperty("spring.profiles.active", "integration-test")
systemProperty("testcontainers.reuse.enable", "true")
maxHeapSize = "2g"
maxParallelForks = 1
}Die Task wird der verification Gruppe zugeordnet, was
sie im gradle tasks Output unter Verification-Tasks
auflistet. Die shouldRunAfter Direktive stellt sicher, dass
Unit-Tests vor Integrationstests laufen, wenn beide ausgeführt werden.
Dies folgt dem Prinzip schneller Tests vor langsamen Tests.
Test-spezifische Konfigurationen optimieren die Ausführung. Der erhöhte Heap-Space accommodiert speicherintensive Integrationstests. Die Parallelität wird auf 1 begrenzt, da Integrationstests oft shared Resources wie Datenbank-Ports verwenden. System-Properties konfigurieren Test-Frameworks und aktivieren Features wie Testcontainer-Reuse.
Die Integration in den Build-Lifecycle erfolgt optional:
tasks.check {
dependsOn(tasks.named("integrationTest"))
}Diese Konfiguration führt Integrationstests bei
gradle check aus, nicht aber bei gradle build.
Dies balanciert zwischen vollständiger Validierung und schnellen
Build-Zeiten.
In CI/CD-Pipelines werden Integrationstests typischerweise als separate Stage ausgeführt. Die Trennung von Unit- und Integrationstests ermöglicht parallele Ausführung und frühe Fehlererkennung. Eine Jenkins-Pipeline könnte folgendermaßen strukturiert sein:
stage('Unit Tests') {
sh './gradlew test'
}
stage('Integration Tests') {
sh './gradlew integrationTest'
}Die separaten Test-Reports ermöglichen granulare Analyse. Gradle
generiert Reports unter build/reports/tests/test und
build/reports/tests/integrationTest. Diese können von
CI-Tools aggregiert und visualisiert werden. Die Trennung hilft bei der
Identifikation, ob Fehler in Unit- oder Integrationstests auftreten.
Caching-Strategien unterscheiden sich zwischen den Test-Typen. Unit-Test-Ergebnisse können aggressiv gecacht werden, da sie deterministisch sind. Integrationstests mit externen Dependencies müssen vorsichtiger gecacht werden. Die Task-Configuration kann Build-Cache explizit deaktivieren:
tasks.named<Test>("integrationTest") {
outputs.upToDateWhen { false }
}Test-Datenmanagement erfordert besondere Aufmerksamkeit bei
Integrationstests. Testcontainers mit Flyway oder Liquibase migrieren
Datenbank-Schemas automatisch. Test-Fixtures in
src/integrationTest/resources/fixtures stellen konsistente
Testdaten bereit. Die @Sql Annotation in Spring Boot Tests
lädt SQL-Skripte für Testdaten-Setup.
Performance-Optimierungen reduzieren Ausführungszeiten erheblich. Testcontainer-Reuse vermeidet wiederholtes Container-Starten. Parallele Test-Ausführung auf Klassen-Ebene statt Methoden-Ebene reduziert Konflikte. Das Gruppieren verwandter Tests in Test-Suites minimiert Setup-Overhead.
Die Dokumentation der Test-Struktur ist essentiell für
Team-Onboarding. Ein README im src/integrationTest
Verzeichnis erklärt Voraussetzungen, Umgebungsvariablen und
Debugging-Tipps. Beispiel-Tests demonstrieren Patterns für
Datenbank-Tests, API-Tests und Message-Queue-Tests. Diese Dokumentation
reduziert Einarbeitungszeit und fördert konsistente Test-Praktiken.