Backend Development 13 min read

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.

Fulu Network R&D Team
Fulu Network R&D Team
Fulu Network R&D Team
Custom Robot Framework Test Report Generation with XML Parsing, Jinja2 Templating, and Email Delivery

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 lst

2.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 cases

2.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 HTML

2.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.

pythonBackend DevelopmentXMLEmailJinja2report-generationrobot-framework
Fulu Network R&D Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.