【超好看】pytest测试报告邮件模版

自定义测试报告

先来看下最终的实现效果:

【超好看】pytest测试报告邮件模版_第1张图片

准备测试报告模版
DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Demotitle>
    <style>
        table {
            border-collapse: collapse;
            width: 80%; 
        }

        th, td {
            border: 1px solid black; 
            padding: 8px; 
            text-align: center;
        }

        th, th {
            border: 1px solid black;
            padding: 8px; 
            text-align: center;
        }
    style>
head>
<body>
各位,好!<br>
  以下为本次自动化测试报告概览内容,请注意查收~<br><br>
注:<br>
 报告在线地址访客账号: xxxx/xxxx<br>
 本邮件由系统自动发出,无需回复,谢谢!

<hr>
<table>
    <tbody>
    <tr>
        <th colspan="8" style="background-color: #009f95">概览th>
    tr>
    <tr>
        <th>报告名称th>
        <th>执行结果th>
        <th colspan="3" style="text-align: center">报告在线地址th>
        <th>开始时间th>
        <th style="width:auto">结束时间th>
        <th>执行耗时th>
    tr>
    <tr>
        <td>Allure报告td>
        <td>${BUILD_STATUS}td>
        <td colspan="3"><a href="${PROJECT_URL}${BUILD_NUMBER}/allure">${PROJECT_URL}${BUILD_NUMBER}/allurea>td>
        <td>s_timetd>
        <td>e_timetd>
        <td>all_timetd>
    tr>

    <tr>
        <th>用例总数th>
        <th>成功个数th>
        <th>失败个数th>
        <th>跳过个数th>
        <th>警告个数th>
        <th>成功率th>
        <th>失败率th>
        <th>备注th>
    tr>

    <tr>
        <td>${TEST_COUNTS,var="total"}td>
        <td>${TEST_COUNTS,var="pass"}td>
        <td>fail_cnttd>
        <td>${TEST_COUNTS,var="skip"}td>
        <td>broken_cnttd>
        <td>pass_pcttd>
        <td>fail_pcttd>
        <td>暂无td>
    tr>
    <tr>
        <th colspan="8" style="background-color: #009f95">异常用例列表th>
    tr>
    tbody>
table>
body>
html>

展示效果如下:

【超好看】pytest测试报告邮件模版_第2张图片

模板中的部分信息直接用字符串代替了,后面通过解析测试结果文件获取开始时间、结束时间、耗时、成功、失败跳过用例信息后替换部分内容,增加异常用例信息。

为什么不直接使用Content Token Reference中的成功、失败、跳过信息呢?

【超好看】pytest测试报告邮件模版_第3张图片

jenkins的变量中有成功、失败、跳过数量,但是这里的告警状态也算到了失败中。它会影响最后计算失败率,这里由于单独统计了告警的数量,所以不使用默认模版的变量。

解析测试结果

虽然这里配置了构建后生成allure报告,但是为了方便读取结果所以在构建时就生成了allure报告,然后自己实现从allure报告中获取执行过程数据。

【超好看】pytest测试报告邮件模版_第4张图片

解析结果生成邮件html脚本:

# -*- coding:utf8 -*-
"""
@ Author: wjlv4
@ File: parse_report.py
@ Time: 2023/12/7
@ Contact: [email protected]
@ Desc: 解析allure报告数据
"""
from datetime import datetime
import json
from pathlib import Path
from os import sep

import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent.parent / 'auto_model'))
from Base.common.operate_log import Log


class ParseReport:
    def __init__(self, report_path, email_tmp_file='template.html'):
        if Path(report_path).exists():
            self.report_path = report_path
            self.cases_file = report_path + sep + 'data' + sep + 'behaviors.json'
            self.detail_dir = report_path + sep + 'data' + sep + 'test-cases'
        else:
            raise NotADirectoryError(f'dir not exist! {report_path}')
        self.email_file = email_tmp_file

    def get_res_data(self):
        # 初始化计数器和其他变量
        res = {
            'all_cnt': 0,
            'pass_cnt': 0,
            'fail_cnt': 0,
            'skip_cnt': 0,
            'broken_cnt': 0,
            'other_cnt': 0,
            'pass_pct': 0,
            'fail_pct': 0,
            'fail_tests': [],
            'skip_tests': [],
            'broken_tests': [],
            'other_tests': [],
            's_time': 0,
            'e_time': 0,
            'all_time': None
        }
        Log.info('开始收集测试报告结果 ...')
        data = self._load_json(self.cases_file)
        # 遍历数据集
        for result in data['children']:
            # 统计测试结果
            if 'status' in result:
                if result['status'] == 'passed':
                    res['pass_cnt'] += 1
                elif result['status'] == 'failed':
                    res['fail_cnt'] += 1
                    res['fail_tests'].append(self.get_overview(result['uid']))
                elif result['status'] == 'skipped':
                    res['skip_cnt'] += 1
                    res['skip_tests'].append(self.get_overview(result['uid']))
                elif result['status'] == 'broken':
                    res['broken_cnt'] += 1
                    res['broken_tests'].append(self.get_overview(result['uid']))
                else:
                    res['other_cnt'] += 1
                    res['other_tests'].append(self.get_overview(result['uid']))

                res['all_cnt'] += 1
                res['pass_pct'] = '%.2f%%' % (res['pass_cnt'] / res['all_cnt'] * 100)
                res['fail_pct'] = '%.2f%%' % (res['fail_cnt'] / res['all_cnt'] * 100)
            # 获取开始和结束时间
            if result['time'] and 'start' in result['time']:
                start = result['time']['start'] // 1000
                if not res['s_time'] or start < res['s_time']:
                    res['s_time'] = start

            if result['time'] and 'stop' in result['time']:
                stop = result['time']['stop'] // 1000
                if not res['e_time'] or stop > res['e_time']:
                    res['e_time'] = stop

        # 计算总耗时
        if res['s_time'] and res['e_time']:
            res['all_time'] = self.format_time_difference(res['e_time'], res['s_time'])
            res['s_time'] = datetime.fromtimestamp(res['s_time']).strftime('%Y-%m-%d
%H:%M:%S'
) res['e_time'] = datetime.fromtimestamp(res['e_time']).strftime('%Y-%m-%d
%H:%M:%S'
) return res @staticmethod def _load_json(file_path): path = Path(file_path).absolute() if not path.exists(): print(f'file not exist! {file_path}') else: return json.load(open(path, 'r', encoding='utf8')) def get_overview(self, uid): """ 根据uid对应对的详情文件获取总览信息 :param uid: :return: """ detail_data = self._load_json(self.detail_dir + sep + uid + '.json') status_msg = detail_data.get('statusMessage', '') msg = status_msg.split(':', 1)[-1] name = detail_data.get('name', '') return name, msg @staticmethod def format_time_difference(timestamp1, timestamp2): time_difference = abs(timestamp2 - timestamp1) # 计算时间差的各个组成部分 hours = time_difference // 3600 minutes = (time_difference % 3600) // 60 seconds = time_difference % 60 hours = str(hours).rjust(2, '0') minutes = str(minutes).rjust(2, '0') seconds = str(seconds).rjust(2, '0') # 根据时间差返回相应的字符串表示 if int(hours) > 0: return f"{hours}:{minutes}:{seconds}" elif int(minutes) > 0: return f"00:{minutes}:{seconds}" else: return f"00:00:{seconds}" def update_file(self): """ 主入口,读取模版文件后生成测试报告文件 :return: """ res_data = self.get_res_data() Log.info('生成邮件模板中 ...') with open(self.email_file, 'r', encoding='utf8') as f_r: content = f_r.read() # 处理时间 new_content = content.replace('s_time', res_data['s_time']).replace('e_time', res_data['e_time']).replace( 'all_time', res_data['all_time']) # 处理百分比 new_content = new_content.replace('fail_cnt', str(res_data['fail_cnt'])).replace('broken_cnt', str(res_data['broken_cnt'])).replace('pass_pct', str( res_data['pass_pct'])).replace('fail_pct', str(res_data['fail_pct'])) # 增加异常用例类型 abnormal_case_temp = 'indexcase_namemark' th_html_temp = '序号失败用例名称备注' abnormal_html = '' if res_data['fail_tests']: f_html = th_html_temp.replace('xss', "background-color: #e81123") for index, f_case in enumerate(res_data['fail_tests'], 1): f_html += abnormal_case_temp.replace('index', str(index)).replace('case_name', f_case[0]).replace( 'mark', f_case[1]) abnormal_html += f_html if res_data['broken_tests']: f_html = th_html_temp.replace('xss', "background-color: #FFEB3B").replace('失败', '警告') for index, f_case in enumerate(res_data['broken_tests'], 1): f_html += abnormal_case_temp.replace('index', str(index)).replace('case_name', f_case[0]).replace( 'mark', f_case[1]) abnormal_html += f_html if res_data['skip_tests']: f_html = th_html_temp.replace('xss', "background-color: #9f9f9f").replace('失败', '跳过') for index, f_case in enumerate(res_data['skip_tests'], 1): f_html += abnormal_case_temp.replace('index', str(index)).replace('case_name', f_case[0]).replace( 'mark', f_case[1]) abnormal_html += f_html with open('email.html', 'w', encoding='utf8') as f_w: f_w.write(new_content.replace(r'', abnormal_html + r'')) Log.info('邮件模板生成完成 ...') if __name__ == '__main__': pr = ParseReport('allure-report') pr.update_file()
配置邮件内容

指定生成的html作为邮件内容发送:

【超好看】pytest测试报告邮件模版_第5张图片

大功告成~

你可能感兴趣的:(jenkins,allure,pytest,servlet,前端)