Gradle generiert standardmäßig Test-Reports in verschiedenen Formaten, die unterschiedliche Analyse-Szenarien unterstützen. Der HTML-Report bietet eine interaktive Übersicht mit Navigation durch Packages, Klassen und einzelne Test-Methoden. XML-Reports im JUnit-Format ermöglichen Tool-Integration und automatisierte Analyse. Binary-Reports speichern vollständige Test-Execution-Daten für nachträgliche Analyse. Diese Multi-Format-Strategie bedient sowohl menschliche Review-Prozesse als auch automatisierte Quality-Gates.
Die Report-Generierung erfolgt automatisch nach Test-Execution, kann aber durch explizite Konfiguration angepasst werden. Output-Directories, Report-Details und Aggregation-Level sind konfigurierbar. Die Balance zwischen Report-Vollständigkeit und Generierungs-Performance erfordert bewusste Entscheidungen. Detaillierte Reports mit Stack-Traces und System-Output unterstützen Debugging, erhöhen aber Speicherbedarf und Generierungszeit erheblich.
tasks.withType<Test>().configureEach {
reports {
html.required.set(true)
xml.required.set(true)
// Custom Report-Locations
html.outputLocation.set(layout.buildDirectory.dir("test-reports/${name}/html"))
xml.outputLocation.set(layout.buildDirectory.dir("test-reports/${name}/xml"))
// JUnit Platform Report
junitXml.required.set(true)
junitXml.outputLocation.set(layout.buildDirectory.dir("test-reports/${name}/junit-platform"))
}
// Test Logging für Report-Details
testLogging {
events = setOf(
TestLogEvent.FAILED,
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_ERROR
)
exceptionFormat = TestExceptionFormat.FULL
showExceptions = true
showCauses = true
showStackTraces = true
// Capture Output für Failed Tests
showStandardStreams = false
afterSuite { suite, result ->
if (suite.parent == null) {
val summary = "Test Results: ${result.resultType} " +
"(${result.testCount} tests, " +
"${result.successfulTestCount} passed, " +
"${result.failedTestCount} failed, " +
"${result.skippedTestCount} skipped)"
val reportFile = file("${buildDir}/test-summary.txt")
reportFile.appendText("$summary\n")
if (result.failedTestCount > 0) {
logger.error(summary)
} else {
logger.lifecycle(summary)
}
}
}
}
}Custom Report Formats erweitern die Standard-Reports um projektspezifische Requirements. Test-Execution-Listener sammeln zusätzliche Metriken während der Test-Ausführung. Performance-Daten, Memory-Usage oder Custom Assertions werden in strukturierten Formaten persistiert. JSON-Reports ermöglichen flexible Tool-Integration und JavaScript-basierte Visualisierungen. Diese Custom Reports ergänzen Standard-Reports mit domänenspezifischen Informationen.
Multi-Projekt-Builds erfordern konsolidierte Test-Reports über alle Module. Die Aggregation erfolgt durch dedizierte Tasks, die Reports aus Subprojekten sammeln und zu einer Gesamt-Übersicht vereinen. Diese Konsolidierung bietet Projekt-weite Test-Statistiken und identifiziert Problembereiche über Modul-Grenzen hinweg.
Die Test-Report-Aggregation-Task sammelt XML-Reports aus allen Subprojekten und generiert einen kombinierten HTML-Report. Die Herausforderung liegt in der korrekten Zuordnung von Test-Results zu Source-Locations über Projekt-Grenzen. Relative Pfade müssen zu absoluten Pfaden aufgelöst werden, um korrekte Source-Links in aggregierten Reports zu gewährleisten.
// Root-Level Test Report Aggregation
val testReportData = configurations.create("testReportData") {
isCanBeConsumed = false
isCanBeResolved = true
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.VERIFICATION))
attribute(VerificationType.VERIFICATION_TYPE_ATTRIBUTE, objects.named(VerificationType.TEST_RESULTS))
}
}
dependencies {
subprojects.forEach { subproject ->
if (subproject.plugins.hasPlugin("java")) {
testReportData(project(subproject.path)) {
capabilities {
requireCapability("${subproject.group}:${subproject.name}-test-results")
}
}
}
}
}
tasks.register<TestReport>("aggregatedTestReport") {
description = "Generates an aggregated test report for all subprojects"
group = "verification"
destinationDirectory.set(layout.buildDirectory.dir("reports/tests/aggregated"))
// Collect test results from all subprojects
testResults.from(subprojects.map { subproject ->
subproject.tasks.withType<Test>().map { it.binaryResultsDirectory }
})
// Generate aggregated metrics
doLast {
val metricsFile = file("${destinationDirectory.get()}/metrics.json")
val metrics = calculateAggregatedMetrics()
metricsFile.writeText(groovy.json.JsonOutput.toJson(metrics))
logger.lifecycle("Aggregated Test Metrics:")
logger.lifecycle(" Total Tests: ${metrics.totalTests}")
logger.lifecycle(" Success Rate: ${metrics.successRate}%")
logger.lifecycle(" Total Duration: ${metrics.totalDuration}ms")
logger.lifecycle(" Slowest Module: ${metrics.slowestModule}")
}
}
// Subproject configuration for test result publication
subprojects {
if (plugins.hasPlugin("java")) {
configurations.create("testResultsElements") {
isCanBeConsumed = true
isCanBeResolved = false
attributes {
attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.VERIFICATION))
attribute(VerificationType.VERIFICATION_TYPE_ATTRIBUTE, objects.named(VerificationType.TEST_RESULTS))
}
outgoing {
capability("${group}:${name}-test-results:${version}")
artifacts(tasks.named<Test>("test").map { it.binaryResultsDirectory })
}
}
}
}Cross-Module-Test-Coverage aggregiert Coverage-Daten über Projekt-Grenzen. Integration-Tests in einem Modul können Code in anderen Modulen ausführen. Die aggregierte Coverage zeigt die tatsächliche Code-Abdeckung über das gesamte System. JaCoCo’s Merge-Task kombiniert Execution-Daten aus verschiedenen Test-Runs. Der aggregierte Report identifiziert untestete Code-Bereiche, die in modularen Tests übersehen wurden.
Die Visualisierung von Test-Metriken transformiert rohe Daten in actionable Insights. HTML-Reports bieten grundlegende Visualisierungen, aber dedizierte Dashboards ermöglichen tiefere Analyse. Grafana, Kibana oder Custom Web-Dashboards konsumieren Test-Report-Daten und visualisieren Trends, Verteilungen und Anomalien. Diese Visualisierungen unterstützen datengetriebene Entscheidungen über Test-Strategie und Code-Qualität.
Time-Series-Visualisierungen zeigen Test-Metriken über Zeit. Success-Rates, Test-Counts und Execution-Times werden als Linien-Diagramme dargestellt. Diese Trends identifizieren Degradation früh und korrelieren Test-Probleme mit Code-Changes. Heatmaps visualisieren Test-Failures über Module und Zeit, was Problem-Cluster aufdeckt. Distribution-Charts zeigen Test-Duration-Verteilungen und identifizieren Performance-Outliers.
// Test Metrics Export für Visualisierung
tasks.register("exportTestMetrics") {
description = "Export test metrics for visualization"
dependsOn(tasks.withType<Test>())
val metricsDir = file("${buildDir}/test-metrics")
outputs.dir(metricsDir)
doLast {
metricsDir.mkdirs()
// Prometheus-Format Export
val prometheusFile = file("${metricsDir}/test_metrics.prom")
prometheusFile.writeText(buildString {
appendLine("# HELP test_duration_seconds Test execution duration")
appendLine("# TYPE test_duration_seconds histogram")
tasks.withType<Test>().forEach { testTask ->
val results = testTask.reports.xml.outputLocation.get().asFile
.walkTopDown()
.filter { it.extension == "xml" }
.flatMap { parseTestResults(it) }
.toList()
results.forEach { result ->
appendLine("test_duration_seconds{module=\"${project.name}\",test=\"${result.name}\"} ${result.duration}")
}
}
appendLine("# HELP test_success_total Number of successful tests")
appendLine("# TYPE test_success_total counter")
appendLine("test_success_total{project=\"${project.name}\"} ${countSuccessfulTests()}")
appendLine("# HELP test_failure_total Number of failed tests")
appendLine("# TYPE test_failure_total counter")
appendLine("test_failure_total{project=\"${project.name}\"} ${countFailedTests()}")
})
// InfluxDB Line Protocol Export
val influxFile = file("${metricsDir}/test_metrics.influx")
val timestamp = System.currentTimeMillis() * 1000000 // nanoseconds
influxFile.writeText(buildString {
tasks.withType<Test>().forEach { testTask ->
val moduleName = testTask.project.name.replace(" ", "\\ ")
appendLine("test_execution,module=${moduleName},task=${testTask.name} " +
"duration=${testTask.duration}i," +
"passed=${testTask.passedTestCount}i," +
"failed=${testTask.failedTestCount}i," +
"skipped=${testTask.skippedTestCount}i " +
timestamp)
}
})
}
}Dashboard-Integration erfolgt über REST APIs oder File-basierte Imports. Elasticsearch konsumiert JSON-Reports für Full-Text-Search über Test-Results. Grafana-Dashboards visualisieren Prometheus-Metrics mit Alerts für Threshold-Violations. Jenkins oder GitLab zeigen Test-Trends in Build-Pipelines. Diese Integrationen machen Test-Metriken zum integralen Teil des Development-Workflows.
Test-Failure-Analysis identifiziert Muster und Root-Causes von Test-Problemen. Die Analyse beginnt mit Failure-Categorization: Compilation-Errors, Assertion-Failures, Timeouts oder Infrastructure-Problems. Jede Kategorie erfordert unterschiedliche Debugging-Strategien. Automated Categorization basiert auf Exception-Types und Error-Messages in Test-Reports.
Failure-Reports fokussieren auf actionable Information für Entwickler. Stack-Traces werden auf relevante Frames reduziert, Framework-Code wird ausgeblendet. Assertion-Failures zeigen Expected versus Actual Values mit Diff-Visualisierung. Screenshots oder DOM-Dumps von UI-Tests dokumentieren den Failure-State. Diese kontextreiche Information beschleunigt Problem-Resolution erheblich.
tasks.register("analyzeTestFailures") {
description = "Analyze and categorize test failures"
mustRunAfter(tasks.withType<Test>())
val analysisReport = file("${buildDir}/reports/test-failure-analysis.html")
outputs.file(analysisReport)
doLast {
val failures = mutableMapOf<String, MutableList<TestFailure>>()
// Parse test results
tasks.withType<Test>().forEach { testTask ->
testTask.reports.xml.outputLocation.get().asFile
.walkTopDown()
.filter { it.extension == "xml" }
.forEach { xmlFile ->
val testResults = XmlParser().parse(xmlFile)
testResults.depthFirst().filter {
it.name() == "testcase" && it.children().any { child ->
(child as? Node)?.name() in listOf("failure", "error")
}
}.forEach { testCase ->
val failure = parseTestFailure(testCase as Node)
failures.getOrPut(failure.category) { mutableListOf() }.add(failure)
}
}
}
// Generate HTML Report
analysisReport.writeText("""
<!DOCTYPE html>
<html>
<head>
<title>Test Failure Analysis</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.category { margin-bottom: 30px; }
.failure { background: #fff3cd; padding: 10px; margin: 10px 0; border-left: 4px solid #ffc107; }
.critical { border-left-color: #dc3545; background: #f8d7da; }
pre { background: #f4f4f4; padding: 10px; overflow-x: auto; }
</style>
</head>
<body>
<h1>Test Failure Analysis Report</h1>
<p>Generated: ${Instant.now()}</p>
${failures.map { (category, failureList) -> """
<div class="category">
<h2>$category (${failureList.size} failures)</h2>
${failureList.take(10).joinToString("\n") { failure -> """
<div class="failure ${if (failure.critical) "critical" else ""}">
<h3>${failure.testName}</h3>
<p><strong>Module:</strong> ${failure.module}</p>
<p><strong>Message:</strong> ${failure.message}</p>
<pre>${failure.stackTrace.take(500)}...</pre>
</div>
"""}}
${if (failureList.size > 10) "<p>... and ${failureList.size - 10} more</p>" else ""}
</div>
"""}.joinToString("\n")}
</body>
</html>
""".trimIndent())
logger.lifecycle("Test Failure Analysis: ${analysisReport.toURI()}")
failures.forEach { (category, failureList) ->
logger.lifecycle(" $category: ${failureList.size} failures")
}
}
}Intelligent Failure-Grouping identifiziert Related Failures. Failures mit ähnlichen Stack-Traces oder Error-Messages werden gruppiert. Diese Grouping reduziert Noise und fokussiert auf Unique Problems. Machine-Learning-Ansätze können Historical Failure-Data nutzen, um Known Issues zu identifizieren und Resolution-Suggestions zu bieten.
Test-History-Tracking ermöglicht Long-Term Quality-Monitoring. Jeder Build persistiert Test-Metrics in einer Time-Series-Database oder strukturierten Logs. Diese Historical Data unterstützt Trend-Analysis, Regression-Detection und Performance-Baseline-Establishment. Die Correlation zwischen Test-Metrics und Code-Changes identifiziert Quality-Impact von Features oder Refactorings.
Trend-Reports visualisieren Test-Suite-Evolution über Zeit. Test-Count-Growth zeigt Increasing Coverage oder Technical Debt. Execution-Time-Trends identifizieren Performance-Degradation. Flakiness-Trends tracken Test-Stability. Diese Metriken informieren Decisions über Test-Infrastructure-Investment und Refactoring-Priorities.
tasks.register("generateTrendReport") {
description = "Generate test trend analysis report"
val historyDir = file("${rootProject.buildDir}/test-history")
val trendReport = file("${buildDir}/reports/test-trends.html")
inputs.dir(historyDir)
outputs.file(trendReport)
doLast {
// Collect historical data
val history = historyDir.walkTopDown()
.filter { it.name.endsWith(".json") }
.map { file ->
val data = groovy.json.JsonSlurper().parseText(file.readText()) as Map<String, Any>
TestRunHistory(
timestamp = Instant.parse(data["timestamp"] as String),
totalTests = data["totalTests"] as Int,
passRate = data["passRate"] as Double,
duration = data["duration"] as Long,
modules = data["modules"] as Map<String, Any>
)
}
.sortedBy { it.timestamp }
.toList()
// Calculate trends
val trends = calculateTrends(history)
// Generate visualization
trendReport.writeText("""
<!DOCTYPE html>
<html>
<head>
<title>Test Trend Analysis</title>
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
</head>
<body>
<h1>Test Suite Trend Analysis</h1>
<div id="testCountChart"></div>
<div id="passRateChart"></div>
<div id="durationChart"></div>
<script>
const dates = ${history.map { it.timestamp }.toJson()};
// Test Count Trend
Plotly.newPlot('testCountChart', [{
x: dates,
y: ${history.map { it.totalTests }.toJson()},
type: 'scatter',
name: 'Total Tests'
}], {
title: 'Test Count Over Time',
xaxis: { title: 'Date' },
yaxis: { title: 'Number of Tests' }
});
// Pass Rate Trend
Plotly.newPlot('passRateChart', [{
x: dates,
y: ${history.map { it.passRate }.toJson()},
type: 'scatter',
name: 'Pass Rate',
line: { color: 'green' }
}], {
title: 'Test Pass Rate Trend',
xaxis: { title: 'Date' },
yaxis: { title: 'Pass Rate (%)', range: [0, 100] }
});
// Duration Trend
Plotly.newPlot('durationChart', [{
x: dates,
y: ${history.map { it.duration / 1000 }.toJson()},
type: 'scatter',
name: 'Test Duration',
line: { color: 'blue' }
}], {
title: 'Test Execution Duration',
xaxis: { title: 'Date' },
yaxis: { title: 'Duration (seconds)' }
});
</script>
<h2>Trend Summary</h2>
<ul>
<li>Test Growth Rate: ${trends.growthRate}% per week</li>
<li>Average Pass Rate: ${trends.avgPassRate}%</li>
<li>Pass Rate Trend: ${trends.passRateTrend}</li>
<li>Performance Trend: ${trends.performanceTrend}</li>
</ul>
</body>
</html>
""".trimIndent())
}
}
// Persist current test run for historical tracking
tasks.withType<Test>().configureEach {
finalizedBy(tasks.register("persistTestHistory${name.capitalize()}") {
val historyDir = file("${rootProject.buildDir}/test-history")
val historyFile = file("${historyDir}/${project.name}-${name}-${System.currentTimeMillis()}.json")
doLast {
historyDir.mkdirs()
val metrics = mapOf(
"timestamp" to Instant.now().toString(),
"project" to project.name,
"task" to name,
"totalTests" to testCount,
"passRate" to if (testCount > 0) (passedTestCount * 100.0 / testCount) else 100.0,
"duration" to duration,
"passedTests" to passedTestCount,
"failedTests" to failedTestCount,
"skippedTests" to skippedTestCount
)
historyFile.writeText(groovy.json.JsonOutput.toJson(metrics))
}
})
}Predictive Analytics nutzen Historical Data für Forecasting. Machine-Learning-Models trainieren auf Test-History und predicten Failure-Likelihood, Execution-Time oder Required Resources. Diese Predictions optimieren Test-Selection, Resource-Allocation und Pipeline-Scheduling. Anomaly-Detection identifiziert Unusual Test-Behavior früh und triggert Investigations bevor Problems escalieren.