Custom Robot Framework Test Report Generation with XML Parsing, Jinja2 Templating, and Email Delivery
This article describes a complete solution for generating customized Robot Framework test reports by parsing the output.xml file, extracting test case statistics and details with Python, rendering them into an HTML template using Jinja2, and finally sending the report via email.
1. Background
Robot Framework (RF) result reports provide a convenient overview of test case execution statistics, but they cannot directly display custom data comparisons required by some projects. The native report format therefore does not meet these needs.
Native report
Project‑specific report format
2. Solution
2.1 Process Flow
Parse output.xml to obtain test case information and execution results.
Retrieve additional data via API or database.
Combine the two data sources into a list for template rendering.
Design an HTML report template.
Render the template with the data to generate the final report file.
Send the report by email.
2.2 output.xml Parsing
The execution results of each test case can be extracted by parsing RF's output.xml file.
2.2.1 Statistics of Test Execution
import xml.dom.minidom
import xml.etree.ElementTree
# Open XML document
dom = xml.dom.minidom.parse('E:\\robot\\fightdata_yuce\\results\\output.xml')
root2 = xml.etree.ElementTree.parse('E:\\robot\\fightdata_yuce\\results\\output.xml')
# Get document element
root = dom.documentElement
total = root.getElementsByTagName('total');
total_len = len(total)
# Number of
nodes under
total2 = root2.getiterator("total")
total_stat_num = len(total2[total_len-1].getchildren())
statlist = root.getElementsByTagName('stat');
def get_total_statistics():
lst = []
for i in range(0, total_stat_num):
d = {}
d['fail'] = int(statlist[i].getAttribute("fail")) # failed cases
d['pass'] = int(statlist[i].getAttribute("pass")) # passed cases
d['total'] = d['fail'] + d['pass'] # total cases
d['percent'] = ('{:.2%}'.format(d['pass'] / d['total'])) # pass rate
lst.append(d)
return lst2.2.2 Test Case Details
Test case hierarchy
Each suite contains four cases
import xml.dom.minidom
import xml.etree.ElementTree
# Open XML document
dom = xml.dom.minidom.parse('E:\\robot\\xxx\\results\\output.xml')
root2 = xml.etree.ElementTree.parse('E:\\robot\\xxx\\results\\output.xml')
tree3 = root2.getroot()
# Get children under each suite
def getcase():
casedict = {}
testlist2 = []
for elem in tree3.iterfind('suite/suite'):
a = elem.attrib
suitedict = {}
testlist2.append(suitedict) # store each suite
testlist = []
suitedict['suitename'] = a['name']
for test in elem.iter(tag='test'):
b = test.attrib
for data in test.iterfind('status'):
casename = b['name']
c = data.attrib
status = c['status']
casedict['casename'] = casename
casedict['status'] = status
testlist.append(casedict)
casedict = {}
suitedict['test'] = testlist
return testlist2 # returns list of suites with their cases2.3 Data Filling
After obtaining the data, it is injected into a Jinja2 template to produce a data‑rich HTML report.
from jinja2 import Environment, FileSystemLoader
import parsexml
def generate_html(data):
env = Environment(loader=FileSystemLoader('./')) # load template
template = env.get_template('report.html')
data = parsexml.get_total_statistics() # statistics
data2 = parsexml.getcase() # case details
with open("result.html", 'w', encoding='utf-8') as fout:
html_content = template.render(data=data, data2=data2)
fout.write(html_content) # write rendered HTML2.4 Jinja2 Template Overview
The Jinja2 template is a regular HTML file where placeholders are replaced with the provided data during rendering.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Custom RF Report</title>
</head>
<body>
<div style="width:100%;float:left">
<table cellspacing="0" cellpadding="4" border="1" align="left">
<thead>
<tr bgcolor="#F3F3F3">
<td style="text-align:center" colspan="9"><b>Automated Daily Report</b></td>
</tr>
... (table header rows) ...
</thead>
<tbody>
{% for data in data2 %}
<tr><td colspan="9"><b>{{data['suitename']}}</b></td></tr>
{% for c2 in data['test'] %}
<tr>
<td colspan="2">{{c2['casename']}}</td>
{% if c2['status']=='PASS' %}
<td><b><span style="color:#66CC00">{{c2['status']}}</span></b></td>
{% else %}
<td><b><span style="color:#FF3333">{{c2['status']}}</span></b></td>
{% endif %}
<td>{{c2['max']}}</td>
<td>{{c2['min']}}</td>
{% if c2['casename']=='01 GMV' %}
<td>{{c2['yhat']}}</td>
{% else %}
<td>--</td>
{% endif %}
<td>{{c2['real']}}</td>
{% if c2['casename']=='01 GMV' %}
<td>{{c2['reduce']}}</td>
{% else %}
<td>--</td>
{% endif %}
{% if c2['casename']=='01 GMV' %}
<td>{{c2['percent']}}</td>
{% else %}
<td>--</td>
{% endif %}
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>2.5 Email Sending
The generated HTML report is sent as an email attachment.
#!/usr/bin/python
# -*- coding: utf-8 -*-
import smtplib, time, os
from email.mime.text import MIMEText
from email.header import Header
import generate
def send_mail_html(file):
sender = '[email protected]' # sender
mail_to = ['[email protected]','[email protected]'] # recipients
t = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
subject = 'Automated RF Report ' + t
smtpserver = 'smtp.qiye.aliyun.com'
username = '[email protected]'
password = '123456'
with open(file, 'rb') as f:
mail_body = f.read()
msg = MIMEText(mail_body, _subtype='html', _charset='utf-8')
msg['Subject'] = Header(subject, 'utf-8')
msg['From'] = sender
msg['To'] = ";".join(mail_to)
try:
smtp = smtplib.SMTP()
smtp.connect(smtpserver)
smtp.login(username, password)
smtp.sendmail(sender, mail_to, msg.as_string())
except Exception:
print("Email sending failed!")
else:
print("Email sent successfully!")
finally:
smtp.quit()
def result():
file = 'result.html' # rendered report
result = {}
generate.generate_html(result)
send_mail_html(file)3. Review of the Implementation Process
Initially we searched for existing solutions and found that Jenkins’ built‑in RF plugin could parse reports and send emails, which we used for the first version. However, the plugin only exposed basic fields (name, status, time, error message) and could not display custom data for each case.
We then discovered Jinja2, which allowed us to create a fully custom HTML template and populate it with any data extracted from output.xml . This approach gave us the flexibility to meet personalized reporting requirements while still using Jenkins to trigger the test jobs.
We are now looking to containerize the RF execution environment; any related experience or guidance would be appreciated.
Fulu Network R&D Team
Providing technical literature sharing for Fulu Holdings' tech elite, promoting its technologies through experience summaries, technology consolidation, and innovation sharing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.