Test-Profile ermöglichen die Ausführung verschiedener Test-Sets für unterschiedliche Szenarien. Ein Fast-Feedback-Profil führt nur kritische Unit-Tests während der Entwicklung aus, während ein Full-Regression-Profil alle Tests für CI/CD-Pipelines umfasst. Diese Differenzierung balanciert Feedback-Geschwindigkeit mit Test-Vollständigkeit. Gradle implementiert Profile durch Custom Tasks, System Properties oder Project Properties, die Test-Execution beeinflussen.
Die Konfiguration erfolgt über Gradle Properties, die verschiedene Test-Aspekte steuern. Properties definieren, welche Test-Kategorien ausgeführt werden, welche Ressourcen verfügbar sind und wie Tests parallelisiert werden. Environment-spezifische Properties passen Tests an lokale Entwicklungsumgebungen, CI-Server oder Cloud-Infrastrukturen an. Die Hierarchie von Properties ermöglicht Defaults mit gezielten Overrides.
// Test-Profile Definition
val testProfiles = mapOf(
"fast" to TestProfile(
includeTags = setOf("fast", "unit"),
excludeTags = setOf("slow", "integration", "external"),
parallel = true,
maxForks = 4,
timeout = Duration.ofMinutes(5)
),
"integration" to TestProfile(
includeTags = setOf("integration"),
excludeTags = setOf("external"),
parallel = false,
maxForks = 1,
timeout = Duration.ofMinutes(30)
),
"full" to TestProfile(
includeTags = setOf(),
excludeTags = setOf(),
parallel = true,
maxForks = 8,
timeout = Duration.ofHours(2)
)
)
val activeProfile = project.findProperty("testProfile") as String? ?: "fast"
val profile = testProfiles[activeProfile] ?: error("Unknown test profile: $activeProfile")
tasks.test {
useJUnitPlatform {
if (profile.includeTags.isNotEmpty()) {
includeTags(*profile.includeTags.toTypedArray())
}
if (profile.excludeTags.isNotEmpty()) {
excludeTags(*profile.excludeTags.toTypedArray())
}
}
maxParallelForks = if (profile.parallel) profile.maxForks else 1
timeout.set(profile.timeout)
// Profile-spezifische System Properties
systemProperty("test.profile", activeProfile)
systemProperty("test.db.url",
if (activeProfile == "integration")
"jdbc:postgresql://localhost:5432/test"
else
"jdbc:h2:mem:test"
)
}External Service Configuration unterscheidet zwischen echten Services und Mocks. Development-Tests verwenden WireMock oder embedded Services für Isolation. Integration-Tests verbinden zu dedizierten Test-Instanzen externer Services. Production-like Tests nutzen Staging-Umgebungen mit realistischen Daten und Last. Die Konfiguration erfolgt über Environment-Variablen oder Service-Discovery-Mechanismen.
Systematisches Test-Tagging ermöglicht granulare Kontrolle über
Test-Execution. Tags kategorisieren Tests nach verschiedenen Dimensionen
wie Geschwindigkeit, Stabilität, Feature-Bereich oder Required
Resources. JUnit 5’s @Tag Annotation markiert Test-Klassen
oder -Methoden. Diese Kategorisierung unterstützt selective
Test-Execution basierend auf Context und Requirements.
Die Tag-Hierarchie sollte orthogonale Dimensionen abbilden.
Speed-Tags (@Tag("fast"), @Tag("slow"))
klassifizieren nach Ausführungszeit. Stability-Tags
(@Tag("stable"), @Tag("flaky")) identifizieren
problematische Tests. Feature-Tags (@Tag("authentication"),
@Tag("payment")) gruppieren nach Funktionalität.
Resource-Tags (@Tag("database"),
@Tag("network")) deklarieren externe Dependencies.
// Tag-basierte Test-Tasks
val tagBasedTasks = mapOf(
"fast" to setOf("fast"),
"slow" to setOf("slow"),
"stable" to setOf("stable"),
"database" to setOf("database"),
"external" to setOf("external")
)
tagBasedTasks.forEach { (name, tags) ->
tasks.register<Test>("test${name.capitalize()}") {
group = "verification"
description = "Run ${name} tests"
useJUnitPlatform {
includeTags(*tags.toTypedArray())
}
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
}
}
// Composite Tasks für Test-Suites
tasks.register("testCritical") {
description = "Run critical tests for pre-commit validation"
dependsOn("testFast", "testStable")
}
tasks.register("testNightly") {
description = "Run comprehensive nightly test suite"
dependsOn(tasks.withType<Test>())
}Meta-Annotations reduzieren Tagging-Boilerplate und standardisieren
Test-Kategorien. Custom Annotations kombinieren mehrere Tags und
JUnit-Annotations zu semantischen Test-Markern.
@IntegrationTest impliziert
@Tag("integration"), @Tag("slow") und
@TestInstance(Lifecycle.PER_CLASS). Diese Abstraktion
vereinfacht Test-Authoring und enforced Conventions.
Flaky Tests unterminieren Vertrauen in Test-Suites und verzögern Development. Diese Tests schlagen intermittierend ohne Code-Änderungen fehl, typischerweise durch Race Conditions, Timing-Issues oder External Dependencies. Die Identifikation erfolgt durch Test-History-Analysis und Retry-Patterns. Gradle’s Test-Retry-Plugin wiederholt fehlgeschlagene Tests und markiert Flaky-Kandidaten.
Die Isolation flaky Tests verhindert Build-Blockierung während der
Stabilisierung. Ein separates flaky-Tag excludiert diese
Tests von Standard-Builds. Ein dedizierter testFlaky-Task
führt nur diese Tests aus, möglicherweise mit mehreren Retries.
CI/CD-Pipelines können Flaky-Test-Results als Warnings statt Failures
behandeln. Diese Strategie balanciert Stabilität mit der Notwendigkeit,
Problems zu identifizieren.
// Flaky Test Management
tasks.test {
useJUnitPlatform {
excludeTags("flaky")
}
}
val testFlaky = tasks.register<Test>("testFlaky") {
description = "Run flaky tests with retries"
group = "verification"
useJUnitPlatform {
includeTags("flaky")
}
// Aggressive Retry-Strategie für Flaky Tests
retry {
maxRetries = 5
maxFailures = 20
failOnPassedAfterRetry = false
}
// Logging für Flaky-Analysis
testLogging {
events("failed", "passed", "skipped", "standard_error")
showExceptions = true
showCauses = true
showStackTraces = true
}
// Report Flaky-Results separat
reports.html.outputLocation.set(layout.buildDirectory.dir("reports/flaky-tests"))
}
// Flaky-Test-Detection Task
tasks.register("detectFlakyTests") {
description = "Analyze test results for flaky behavior"
doLast {
val testResults = fileTree(buildDir) {
include("test-results/*/TEST-*.xml")
}
val flakyTests = analyzeFlakyPatterns(testResults)
if (flakyTests.isNotEmpty()) {
logger.warn("Detected ${flakyTests.size} potentially flaky tests:")
flakyTests.forEach { logger.warn(" - $it") }
file("${buildDir}/reports/flaky-tests.txt").writeText(
flakyTests.joinToString("\n")
)
}
}
}Root-Cause-Analysis für Flaky Tests erfordert systematisches Debugging. Thread-Dumps während Test-Execution identifizieren Deadlocks. Increased Logging Level reveal Timing-Dependencies. Test-Isolation durch Fresh JVMs oder Containers eliminiert State-Pollution. Deterministic Test-Data und Clock-Mocking addressieren Time-Based Flakiness.
Test-Performance bestimmt Developer-Productivity und CI/CD-Efficiency. Langsame Tests verzögern Feedback und reduzieren Iteration-Speed. Die Optimierung beginnt mit Performance-Profiling zur Identifikation von Bottlenecks. Gradle Build Scans visualisieren Test-Duration und identifizieren die langsamsten Tests. Diese Information priorisiert Optimierungs-Efforts.
In-Memory-Databases beschleunigen datenbankintensive Tests erheblich. H2 oder HSQLDB ersetzen PostgreSQL oder MySQL für Unit-Tests. Die Schema-Compatibility wird durch gemeinsame SQL-Standards oder Compatibility-Modi sichergestellt. Integration-Tests verwenden weiterhin Real Databases via Testcontainers, aber mit Optimierungen wie Shared Containers oder Database Snapshots.
// Performance-optimierte Test-Konfiguration
tasks.withType<Test>().configureEach {
// JVM-Optimierungen
jvmArgs(
"-XX:+UseG1GC",
"-XX:MaxGCPauseMillis=100",
"-XX:+ParallelRefProcEnabled",
"-XX:+UseStringDeduplication",
"-Djava.security.egd=file:/dev/./urandom", // Faster random generation
"-Dspring.test.context.cache.maxSize=128" // Spring Context Caching
)
// Disable unnötige Features während Tests
systemProperty("spring.jmx.enabled", "false")
systemProperty("spring.devtools.restart.enabled", "false")
systemProperty("logging.level.root", "WARN")
// Build-Cache für Test-Results
outputs.cacheIf { true }
outputs.upToDateWhen { false } // Force cache validation
// Test-Class-Ordering für bessere Parallelisierung
systemProperty("junit.jupiter.testclass.order.default",
"org.junit.jupiter.api.ClassOrderer\$Random")
}
// Shared Test-Resources
val sharedTestResources = tasks.register("setupSharedTestResources") {
outputs.dir("${buildDir}/shared-test-resources")
doLast {
// Start shared services (DB, Redis, etc.)
startSharedTestContainers()
// Generate expensive test data once
generateExpensiveTestData()
}
}
tasks.test {
dependsOn(sharedTestResources)
doFirst {
// Pass shared resource locations to tests
systemProperty("shared.db.url", getSharedDbUrl())
systemProperty("shared.redis.url", getSharedRedisUrl())
}
}Test-Parallelization auf verschiedenen Levels maximiert
Resource-Utilization. Class-Level-Parallelization via
maxParallelForks nutzt Multiple CPUs.
Method-Level-Parallelization via JUnit Platform Configuration erhöht
Concurrency innerhalb JVMs. Die Balance zwischen Parallelization-Levels
hängt von Test-Characteristics und Available Resources ab.
Test-Data-Lifecycle-Management verhindert State-Pollution und
garantiert Test-Isolation. Transactional Tests mit Automatic Rollback
sind der Standard für Database-Tests. Spring’s
@Transactional oder JUnit’s @Rollback
Annotations managen Transaction-Boundaries. Diese Strategie funktioniert
für die meisten Tests, versagt aber bei Multi-Transaction-Scenarios oder
External Service Calls.
Explicit Cleanup-Strategies adressieren Non-Transactional Resources.
@AfterEach Methods bereinigen Test-spezifische Daten.
@AfterAll Methods cleanup Class-Level Resources.
Try-Finally-Blocks garantieren Cleanup auch bei Test-Failures.
Resource-Management mit try-with-resources oder Kotlin’s
use automatisiert Lifecycle-Management.
// Test-Cleanup-Tasks
tasks.register("cleanTestData") {
description = "Clean up test data and resources"
doLast {
// Clean test databases
file("${buildDir}/test-db").deleteRecursively()
// Clean test file uploads
file("${buildDir}/test-uploads").deleteRecursively()
// Clean test caches
file("${System.getProperty("java.io.tmpdir")}/test-cache").deleteRecursively()
}
}
tasks.test {
finalizedBy("cleanTestData")
}
// Global Test-Listener für Cleanup
dependencies {
testImplementation(files("${buildDir}/generated/test-listeners"))
}
tasks.register("generateTestListeners") {
val outputDir = file("${buildDir}/generated/test-listeners")
outputs.dir(outputDir)
doLast {
outputDir.mkdirs()
// Generate JUnit Platform Launcher Listener
file("$outputDir/META-INF/services/org.junit.platform.launcher.TestExecutionListener")
.apply { parentFile.mkdirs() }
.writeText("com.company.test.GlobalTestCleanupListener")
// Generate Listener Implementation
file("$outputDir/com/company/test/GlobalTestCleanupListener.java").apply {
parentFile.mkdirs()
writeText("""
package com.company.test;
public class GlobalTestCleanupListener implements TestExecutionListener {
@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult result) {
if (testIdentifier.isTest()) {
TestDataCleaner.cleanTestSpecificData(testIdentifier);
}
}
}
""".trimIndent())
}
}
}Database-Seeding-Strategies initialisieren Consistent Test-States. Liquibase oder Flyway Migrations erstellen Schema und Reference-Data. DBUnit oder SQL-Scripts laden Test-specific Data. Database Snapshots via Docker Volumes ermöglichen Fast Reset zu Known States. Diese Kombinationen balancieren Setup-Speed mit Data-Complexity.
Continuous Testing führt relevante Tests automatisch bei
Code-Änderungen aus. Gradle’s --continuous Flag überwacht
Source-Files und triggert Test-Execution bei Änderungen. Die
Test-Selection basiert auf Dependency-Analysis zwischen Source und
Test-Files. Diese Immediate Feedback Loop beschleunigt Test-Driven
Development und reduziert Context-Switching.
Die Test-Watch-Konfiguration optimiert für schnelles Feedback. Nur affected Tests werden ausgeführt, basierend auf geänderten Klassen und deren Dependents. Test-Prioritization führt Failed Tests zuerst aus. Fast Tests laufen vor Slow Tests. Diese Strategien maximieren den Wert frühen Feedbacks während der Entwicklung.
// Continuous Testing Configuration
tasks.register("watchTest") {
description = "Run tests in watch mode with smart selection"
doLast {
gradle.startParameter.isContinuous = true
// Configure for optimal watch mode
project.extra["test.watch.mode"] = true
project.extra["test.watch.strategy"] = "affected"
project.extra["test.watch.priority"] = "failed-first"
}
finalizedBy(tasks.test)
}
// Test Selection basierend auf Git-Änderungen
tasks.register<Test>("testChanged") {
description = "Run tests affected by uncommitted changes"
val changedFiles = providers.exec {
commandLine("git", "diff", "--name-only", "HEAD")
}.standardOutput.asText.get().lines()
val affectedTests = calculateAffectedTests(changedFiles)
useJUnitPlatform()
// Dynamische Test-Filter
filter {
affectedTests.forEach { includeTestsMatching(it) }
}
// Fail fast bei ersten Fehler
failFast = true
}IDE-Integration verbessert Continuous Testing Experience. IntelliJ IDEA’s Gradle Integration visualisiert Test-Results inline. Test-Runners zeigen Coverage-Information in Editors. Debugging funktioniert nahtlos mit Breakpoints in Test und Production Code. Diese Integration macht Continuous Testing zum natürlichen Teil des Development Workflows.