System Prerequisites and Pipeline Architecture
Before configuring the continuous integration job, verify that Jenkins, JMeter, and the xsltproc command-line utility are installed and accessible on your build agent. The automation flow operates sequentially: Jenkins triggers the execution, JMeter runs the test plan in non-GUI mode to produce a .jtl log, and xsltproc parses the raw XML output through a custom XSL stylesheet to generate a readable HTML summary.
- Jenkins Job Configuration
Create a new Freestyle project in Jenkins. Under Build Triggers, define a cron schedule (e.g., H 2 * * *) to automate nightly execution. In the Builld section, add an Execute Shell or Execute Windows Batch step. Paste the following orchestration script to handle execution and file transformation:
#!/usr/bin/env bash
# Define workspace and tool paths
JMETER_BIN="/opt/jmeter/bin"
TEST_PLAN="${WORKSPACE}/api_validation.jmx"
RAW_LOG="${WORKSPACE}/execution_data.jtl"
XSL_STYLESHEET="${WORKSPACE}/transform_engine.xsl"
HTML_REPORT="${WORKSPACE}/final_report.html"
# Step A: Execute JMeter in non-interactive mode
${JMETER_BIN}/jmeter -n -t "${TEST_PLAN}" -l "${RAW_LOG}" -e -o "${WORKSPACE}/jmeter_metrics"
# Step B: Convert raw JTL log to HTML using XSLT
if [ -f "${RAW_LOG}" ]; then
xsltproc "${XSL_STYLESHEET}" "${RAW_LOG}" > "${HTML_REPORT}"
echo "Report generation complete: ${HTML_REPORT}"
else
echo "Error: JTL log file not found. Aborting report conversion."
exit 1
fi
- Configuring Automated Email Notifications
Leverage the Jenkins Email Extension Plugin to dispatch build outcomes. The template below dynamically injects pipeline variables and embeds the generated HTML report. Configure this in the Post-build Actions section under Editbale Email Notification.
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pipeline Status: ${PROJECT_NAME}#${BUILD_NUMBER}</title>
<style>
:root { --primary: #2c3e50; --success: #27ae60; --fail: #e74c3c; --bg: #ecf0f1; }
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; margin: 0; padding: 20px; background: var(--bg); }
.container { max-width: 900px; margin: auto; background: #fff; padding: 25px; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.status-badge { display: inline-block; padding: 6px 12px; border-radius: 4px; font-weight: bold; }
.pass { background: var(--success); color: #fff; }
.fail { background: var(--fail); color: #fff; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
th, td { padding: 10px; border-bottom: 1px solid #bdc3c7; text-align: left; }
th { background: var(--primary); color: #fff; }
.meta-links a { color: #3498db; text-decoration: none; margin-right: 15px; }
</style>
</head>
<body>
<div class="container">
<h1>Build <span class="status-badge ${BUILD_STATUS == 'SUCCESS' ? 'pass' : 'fail'}">${BUILD_STATUS}</span></h1>
<p>Project: <strong>${PROJECT_NAME}</strong> | Trigger: ${CAUSE}</p>
<div class="meta-links">
<a href="${BUILD_URL}console">View Console Output</a>
<a href="${BUILD_URL}">Open Jenkins Dashboard</a>
</div>
<hr>
<h2>Generated Test Report</h2>
${FILE,path="${WORKSPACE}/final_report.html"}
<p style="color:#7f8c8d; font-size:0.85em; margin-top:30px;">Automated notification from Jenkins CI. Do not reply.</p>
</div>
</body>
</html>
- Custom XSL Transformation Logic
The following XSLT 1.0 stylesheet processes the JTL XML structure. It computes aggregate metrics, groups results by thread name, calculates pass/fail ratios, and isolates failed assertions with response payloads. Variable names and structural layout have been optimized for readability and maintainability.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" indent="yes" encoding="UTF-8" doctype-system="about:legacy-compat"/>
<xsl:param name="docTitle" select="'API Execution Report'"/>
<xsl:param name="showPayloads" select="'y'"/>
<xsl:template match="/testResults">
<html>
<head>
<title><xsl:value-of select="$docTitle"/></title>
<style>
body { font: 14px/1.4 system-ui, sans-serif; color: #222; padding: 20px; }
.panel { background: #fafafa; padding: 15px; margin-bottom: 20px; border: 1px solid #ddd; }
h1 { color: #1a73e8; } h2 { border-bottom: 2px solid #1a73e8; padding-bottom: 5px; margin-top: 30px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th { background: #34495e; color: #fff; padding: 10px; text-align: left; }
td { padding: 10px; border: 1px solid #ccc; background: #fff; }
.err { color: #c0392b; font-weight: bold; }
.toggle { cursor: pointer; color: #2980b9; text-decoration: underline; margin-left: 10px; }
.details { display: none; margin-top: 10px; background: #f0f0f0; padding: 10px; }
</style>
<script>
function toggleSection(id) {
var el = document.getElementById(id);
el.style.display = (el.style.display === 'none' || el.style.display === '') ? 'block' : 'none';
}
</script>
</head>
<body>
<xsl:call-template name="headerBlock"/>
<xsl:call-template name="overallStatus"/>
<xsl:call-template name="aggregateStats"/>
<xsl:call-template name="groupMetrics"/>
<xsl:call-template name="failureBreakdown"/>
</body>
</html>
</xsl:template>
<xsl:template name="headerBlock">
<h1><xsl:value-of select="$docTitle"/></h1>
<p>Execution timestamp: <xsl:value-of select="//sample/@ts"/></p>
</xsl:template>
<xsl:template name="overallStatus">
<xsl:variable name="errorTotal" select="count(//sample[@s='false'])"/>
<h2>
<xsl:choose>
<xsl:when test="$errorTotal = 0">Status: All Tests Passed</xsl:when>
<xsl:otherwise><span class="err">Status: Failures Detected</span> (Count: <xsl:value-of select="$errorTotal"/>)</xsl:otherwise>
</xsl:choose>
</h2>
</xsl:template>
<xsl:template name="aggregateStats">
<h2>Global Summary</h2>
<div class="panel">
<table>
<tr><th>Total Requests</th><th>Errors</th><th>Success Ratio</th><th>Avg Latency (ms)</th></tr>
<xsl:variable name="totalReq" select="count(//sample)"/>
<xsl:variable name="failReq" select="count(//sample[@s='false'])"/>
<xsl:variable name="passReq" select="count(//sample[@s='true'])"/>
<xsl:variable name="successRatio" select="$passReq div $totalReq"/>
<xsl:variable name="avgLatency" select="sum(//sample/@t) div $totalReq"/>
<tr>
<td><xsl:value-of select="$totalReq"/></td>
<td class="err"><xsl:value-of select="$failReq"/></td>
<td><xsl:value-of select="format-number($successRatio, '0.00%')"/></td>
<td><xsl:value-of select="format-number($avgLatency, '0')"/></td>
</tr>
</table></div>
</xsl:template>
<xsl:template name="groupMetrics">
<h2>Metrics by Thread Group</h2>
<table>
<tr><th>Module</th><th>Executions</th><th>Failures</th><th>Pass Rate</th><th>Avg Latency</th></tr>
<xsl:for-each select="//sample[not(@tn = preceding::sample/@tn)]">
<xsl:variable name="groupName" select="@tn"/>
<xsl:variable name="grpTotal" select="count(//sample[@tn=$groupName])"/>
<xsl:variable name="grpFail" select="count(//sample[@tn=$groupName][@s='false'])"/>
<xsl:variable name="grpPass" select="$grpTotal - $grpFail"/>
<xsl:variable name="grpRate" select="$grpPass div $grpTotal"/>
<xsl:variable name="grpAvg" select="sum(//sample[@tn=$groupName]/@t) div $grpTotal"/>
<tr>
<td><xsl:value-of select="$groupName"/></td>
<td><xsl:value-of select="$grpTotal"/></td>
<td><xsl:value-of select="$grpFail"/></td>
<td><xsl:value-of select="format-number($grpRate, '0.00%')"/></td>
<td><xsl:value-of select="format-number($grpAvg, '0')"/></td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
<xsl:template name="failureBreakdown">
<xsl:if test="count(//sample[@s='false']) > 0">
<h2>Assertion Failure Details</h2>
<xsl:for-each select="//sample[@s='false'][not(@lb = preceding::sample/@lb)]">
<xsl:variable name="requestLabel" select="@lb"/>
<h3><xsl:value-of select="@tn"/> → <xsl:value-of select="$requestLabel"/></h3>
<table>
<tr><th>Target URL</th><th>Failure Reason</th><th>Response Payload</th></tr>
<xsl:for-each select="//sample[@lb=$requestLabel][@s='false']">
<tr>
<td><xsl:value-of select="java.net.URL"/></td>
<td><xsl:value-of select="assertionResult/failureMessage"/></td>
<xsl:if test="$showPayloads='y'">
<td><xsl:value-of select="responseData"/></td>
</xsl:if>
</tr>
</xsl:for-each>
</table>
<span class="toggle" onclick="toggleSection('detail_{$requestLabel}')">Show Additional Context</span>
<div id="detail_{$requestLabel}" class="details">
<p>Response code: <xsl:value-of select="@rc"/> | Message: <xsl:value-of select="@rm"/></p>
</div>
</xsl:for-each>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Place the XSL file in the same directory as your Jenkins workspace or adjust the XSL_STYLESHEET path in the build script accordingly. The pipeline will now execute automatically, transform the raw performance data, and distribute a structured HTML report via email upon completion.