【引言】
由於要使用Jmeter做接口自動化測試,Jmeter原生報告展示的內容多是基於性能參數展示;自動化測試不需要這么繁雜的報告;故決定自己開發一個簡單明了的展示報告;
【思路】
利用jmeter執行結果樹,生成xml報告;通過對內容進一步解析生成html報告;
需要做以下幾件事:
代碼目錄結構:
root:
├─jmx
├─jtl
├─reports
│ ├─1640250447
|---sax_xml.py
|---report.py
|---run_jmeter.py
1.jmx用來存放jmeter jmx腳本
2.jtl用於存儲jmeter生成的xml報告
3.reports用於存儲生成的報告 xxxx時間戳目錄\report.html
4. sax_xml.py用於解析jmeter xml並在reports目錄生成報告;
5.run_jmeter.py 用於執行jmeter腳本;
備注:
1.如果你只是想解析報告,不需要第5步文件,只要按照目錄將xml報告存在jtl中即可;
2.如果你想,執行和解析一體,那就需要將jmeter的jmx腳本存到jmx目錄;並且對結果樹進行設置;
--------------------------------------------------------------詳細代碼-------------------------------------------------
1.利用python的jinja2生成報告模版;
【模版html】
<!doctype html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- 上述3個meta標簽*必須*放在最前面,任何其他內容都*必須*跟隨其后! --> <title>report</title> <!-- Bootstrap --> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.css"> <!-- HTML5 shim 和 Respond.js 是為了讓 IE8 支持 HTML5 元素和媒體查詢(media queries)功能 --> <!-- 警告:通過 file:// 協議(就是直接將 html 頁面拖拽到瀏覽器中)訪問頁面時 Respond.js 不起作用 --> <!--[if lt IE 9]> <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script> <![endif]--> <style> .hide { display: none; } .passed { display: ""; } .failed { display: ""; } .collapsed { display: none; } .log { background-color: #e6e6e6; border: 1px solid #e6e6e6; color: black; display: block; font-family: "Courier New", Courier, monospace; height: 230px; overflow-y: scroll; padding: 5px; white-space: pre-wrap; } .expander::after { content: " (用例詳情)"; color: #BBB; font-style: italic; cursor: pointer; } .collapser::after { content: " (隱藏詳情)"; color: #BBB; font-style: italic; cursor: pointer; } </style> </head> <body> <div class="container"> <ol class="breadcrumb"> <li class="active">接口測試報告</li> </ol> <div class="row"> <div class="col-sm-3 col-md-6 col-lg-8"> <p class="text-info">測試人:{{results.tester}}</p> </div> </div> <div class="row"> <div class="col-sm-3 col-md-6 col-lg-8"> <p class="text-info">共【{{results.case_count}}】個用例,執行耗時【{{results.time}}】秒</p> </div> </div> <div class="row"> <div class="col-sm-3 col-md-6 col-lg-8"> <button type="button" class="btn btn-info">用例總數:【{{results.case_count}}】</button> <button type="button" class="btn btn-success">成功:【{{results.success}}】</button> <button type="button" class="btn btn-danger">失敗:【{{results.fail}}】</button> </div> </div> <div class="row"> <div class="col-sm-3 col-md-6 col-lg-8" style="align-content: center; width: 100%;"> <table id="dt_deail" class="table table-hover table-bordered table-responsive"> <caption>報告詳情</caption> <thead> <tr> <th>結果</th> <th>描述</th> <th>執行時間</th> <th>執行耗時(ms)</th> </tr> </thead> <tbody id="tb"> <!--for從后端獲取 tr--> {% for row in results.table %} <!--第一個tr是場景名稱--> {% if row.result %} <tr id="{{ loop.index0 }}.{{ loop.index0 }}" class="res info passed"> {% else %} <tr id="{{ loop.index0 }}.{{ loop.index0 }}" class="res danger failed"> {% endif %} <td> {% if row.result %} Passed {% else %} Failed {% endif %} <span class="expander"></span> </td> <td>{{row.desc}}</td> <td>{{row.case_exec_time}}</td> <td>{{row.time}}</td> </tr> <!--第二個tr是第一個tr的場景詳細--> <tr id={{ loop.index0 }} class="collapsed"> <td colspan="4"> <div class="log">{{row.deail}}</div> </td> </tr> {% endfor %} </tbody> </table> </div> </div> </div> <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依賴 jQuery,所以必須放在前邊) --> <script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script> <!-- 加載 Bootstrap 的所有 JavaScript 插件。你也可以根據需要只加載單個插件。 --> <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script> <!--設置--> <script src="https://unpkg.com/bootstrap-table@1.15.3/dist/bootstrap-table.min.js"></script> <script> $(function() { $("#dt_deail").bootstrapTable({ //是否顯示搜索框 search: true, //是否分頁 pagination: true, // 每頁顯示多少條數據,也就是要顯示多少行 pageSize: 20, //分頁,選擇不同數字會改變上面的pageSize pageList: [5, 10, 15, 20], // //顯示列選擇按鈕 // showColumns: false, // //是否刷新 // showRefresh: false, // //是否可見 // showToggle: false, // //默認英文,如下顯示中文 // locale: "zh-CN", // //不緩存 // cache: false, // //背景色,灰白相間 // striped: false, // //是否顯示模向滾動條 // showFooter: false, // //是否啟用排序 // sortable: false, // //sortOrder: "asc", // //是否啟用點擊選中行 // clickToSelect: false, // //每一行的唯一標識,一般為主鍵列 //uniqueId: "ID", // //是否顯示父子表 // detailView: false, // //開啟單遠,想要獲取被選中行數據必須要有該參數 // singleSelect: true, //單擊行事件 onClickRow: function (row, $element) { // 獲取id let id = $element[0].attributes[0].value; id = id.substr(0, id.indexOf(".")) // 按id隱藏,顯示行 $("#"+id).toggle(); $("#"+id).removeClass("hide"); }, }); $('.btn-success').click(function (){ $(".failed").addClass("hide"); $(".passed").removeClass("hide"); $(".collapsed").addClass("hide"); }); $('.btn-danger').click(function (){ $(".passed").addClass("hide"); $(".collapsed").addClass("hide"); $(".failed").removeClass("hide"); }); $('.btn-info').click(function (){ $(".passed").removeClass("hide"); $(".failed").removeClass("hide"); $(".collapsed").addClass("hide"); }); }); </script> </body> </html>
【生成Html代碼】

import jinja2 import os import io import xml.sax from sax_xml import ReportHandler class Report: @classmethod def report(cls, root_dir: str, report_path: str, result_json: dict) ->dict: """填充報告模版""" env = jinja2.Environment( loader=jinja2.FileSystemLoader(root_dir), extensions=(), autoescape=True ) template = env.get_template("template.html", root_dir) html = template.render({"results": result_json}) output_file = os.path.join( report_path, "report.html" ) with io.open(output_file, 'w', encoding="utf-8") as fp: fp.write(html) print(output_file) return result_json @classmethod def parse_xml_generate_html(cls, xml_file: str, report_path: str) -> dict: """ 從xml解析數據,填充到html模版,並成生html報告 """ # 創建一個 XMLReader parse = xml.sax.make_parser() # turn off namespaces parse.setFeature(xml.sax.handler.feature_namespaces, 0) # 重寫 ContextHandler Handler = ReportHandler() parse.setContentHandler(Handler) parse.parse(xml_file) results = Handler.content() results_json = Report.report(os.getcwd(), report_path, results) return results_json if __name__ == '__main__': # 模版解析格式 # results = { # "tester": "test", # "case_count": 123, # "time": 456, # "success": 123, # "fail": 0, # "error": 0, # "table": [{ # "result": True, # "desc": "123", # "exe_time": "123", # "time": "123", # "case_exec_time":"執行時間", # "deail": "12313" # }, # { # "result": False, # "desc": "333", # "exe_time": "44", # "time": "55", # "case_exec_time":"執行時間", # "deail": "666" # } # ] # } xml_report_file = os.path.join( os.path.join(os.getcwd(), 'jtl'), "1639479506.xml" ) html_report_path = os.path.join(os.getcwd(), 'reports') Report.parse_xml_generate_html(xml_report_file, html_report_path)
2.解析xml代碼

import time import xml.sax import json class ReportHandler(xml.sax.handler.ContentHandler): def __init__(self): # 存儲遍歷每個節點名稱; self.current_data = "" # 存儲請求,響應數據; self.request_data = [] self.response_data = [] self.method = "" self.url = "" # url中帶特列字符,解析時會進行分開遍歷;需要放在list最后進行join拼接; self.url_list = [] self.ts = "" # 臨時存儲線程組結果 self.result = {} # 單接口名稱,即jmeter請求命名; self.sample_name = "" # 線程組名稱 self.group_name = "" # 接口響應,請求,臨時存儲字段; self.response = "" self.request = "" # 存儲所有結果,按結果傳給html模版進行渲染頁面; self.all_result = [] # case總數,case成功總數,case失敗總數 self.case_count = {} self.case_success_count = {} self.case_error_count = {} # 存儲,成功,錯誤,失敗結果; self.success = [] self.error = [] self.failure = [] # 臨時存儲每個每個case結果,{[case1,case2]} self.temp_result = {} # 總體case執行耗時s self.total_time = [] # 單個case執行耗時總時間ms self.case_total_time = {} # 存儲每個請求耗時ms self.t = "" # 單個case開始請求時間; self.case_exec_time = {} self.s = "" def startElement(self, name, attrs): """ 元素開始事件處理 """ self.current_data = name if name == "httpSample": self.ts = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(attrs["ts"])/1000)) self.success.append(eval(str(attrs["s"]).capitalize())) self.sample_name = attrs["lb"] self.group_name = attrs["tn"] self.case_count[attrs["tn"]] = 1 self.t = attrs["t"] self.s = eval(str(attrs["s"]).capitalize()) # 記錄各線程組結果list {”線程級名稱1“:[True, False, True], ”線程級名稱2“:[True, True, True]} if not self.result.get(attrs["tn"]): self.result[attrs["tn"]] = [] self.result[attrs["tn"]].append(eval(str(attrs["s"]).capitalize())) self.total_time.append(int(attrs["t"])) def endElement(self, name): """元素結束事件處理""" if self.current_data == "responseData": if self.response_data: try: self.response = json.dumps( json.loads( "".join(self.response_data) ), sort_keys=True, indent=2 ) except json.JSONDecodeError: self.response = self.response_data else: self.response = "" if self.current_data == "queryString": if self.request_data: try: self.request = json.dumps( json.loads( "".join(self.request_data) ), sort_keys=True, indent=2 ) except json.JSONDecodeError: self.request = self.request_data else: self.request = "" if self.current_data == "httpSample": self.case_success_count[self.group_name].append(eval(str(self.s.capitalize()))) if self.response and self.method and self.url: result_string = '''%(desc)s\n%(ts)s %(method)s 請求:%(url)s\n請求:\n%(request)s\n響應:\n%(response)s\n''' # 單條case上的詳細內容 res = result_string % dict( desc=self.sample_name, ts=self.ts, method=self.method, url=self.url, request=self.request, response=self.response ) # temp_result按線程組名稱為key划分,存組整個線程中請求結果; if not self.temp_result.get(self.group_name): self.temp_result[self.group_name] = [] self.case_total_time[self.group_name] = [] self.case_exec_time[self.group_name] = [] self.case_success_count[self.group_name] = [] self.temp_result[self.group_name].append(res) self.case_total_time[self.group_name].append(int(self.t)) self.case_exec_time[self.group_name].append(self.ts) self.case_success_count[self.group_name].append(self.s) # 恢復初始值 self.method = "" self.url = "" self.ts = "" self.sample_name = "" self.group_name = "" self.response = "" self.request = "" self.t = "" self.s = "" self.request_data = [] self.response_data = [] self.current_data = "" self.url = "" self.url_list = [] def characters(self, content): """內容處理事件""" if self.current_data == "responseData": self.response_data.append(content) if self.current_data == "queryString": self.request_data.append(content) if self.current_data == "method": self.method = content if self.current_data == "java.net.URL": if "http" in content or content: self.url_list.append(content) self.url = ''.join(self.url_list) if self.current_data == "error": self.error.append(eval(str(content).capitalize())) if self.current_data == "failure": self.failure.append(eval(str(content).capitalize())) def content(self): """結果進行結構組合""" # 遍歷按線程組分組的結果; for key, val in self.temp_result.items(): self.all_result.append( { "result": all(self.result[key]), "desc": key, "exe_time": self.total_time, "case_exec_time": self.case_exec_time[key][0], "time": sum(self.case_total_time[key]), "deail": ''.join(val) } ) case_result_count = [all(val) for _, val in self.case_success_count.items()] # 最終所有結果; results = { "tester": "test", "case_count": len(self.case_count.keys()), "time": sum(self.total_time) / 1000, "success": case_result_count.count(True), "fail": case_result_count.count(False), "error": self.error.count(True), "table": self.all_result } return results if __name__ == "__main__": # 創建一個 XMLReader parser = xml.sax.make_parser() # turn off namespaces parser.setFeature(xml.sax.handler.feature_namespaces, 0) # 重寫 ContextHandler Handler = ReportHandler() parser.setContentHandler(Handler) parser.parse("1639479506.xml") print( Handler.content() )
3. jmeter運行腳本

import os import time import requests from report import Report class WX: """企業微信""" @classmethod def send_message(cls, content: str, token: str) -> dict: """ 發送企業微信機器人文本消息 :param content: 推送內容 :param token: 機器人key :return: json {"errcode":0,"errmsg":"ok"} """ url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}".format(token) payload = { "msgtype": "text", "text": { "content": content, } } res = requests.post(url, json=payload) return res.json() class Result: """從statistics.json讀取測試結果""" @classmethod def run_jmeter(cls, timestamp: str, jmx_file_name) -> str: """執行jmeter""" cur = os.getcwd() jtl = os.path.join(cur, "jtl") reports = os.path.join(cur, "reports") # 創建report文件夾 os.makedirs(os.path.join(reports, timestamp)) cmd = "jmeter -n -t {} -Jresultsfile={}".format( os.path.join( os.path.join(cur, "jmx"), jmx_file_name ), os.path.join(jtl, "{}.xml".format(timestamp)) ) os.system(cmd) resultsfile = os.path.join(jtl, "{}.xml".format(timestamp)) return resultsfile @classmethod def get_test_result(cls, timestamp: str, desc: str, url: str, jmx_file_name: str) -> str: """ 從jmeter執行結果timeStamp.xml中讀取結果; """ cur_path = os.getcwd() tmpl = """%(desc)s【已完成】:\n共%(case)s個接口, 執行耗時 %(used_time)ss, 通過 %(Pass)s, 失敗 %(error)s, 通過率 %(rate)s \n測試報告地址:%(url)s""" # 執行jmeter cls.run_jmeter(timestamp, jmx_file_name) report_dir = os.path.join( os.path.join(cur_path, "reports"), timestamp ) xml_report_file = os.path.join( os.path.join("jtl", "{}.xml".format(timestamp)) ) results_json = Report.parse_xml_generate_html(xml_report_file, report_dir) # 報告成功率 total_count = results_json['case_count'] error_count = int(results_json['case_count']) - int(results_json['success'] - int(results_json['fail'])) success_count = int(results_json['success']) success_rate = success_count / total_count * 100 ret = tmpl % dict( desc=desc, case=str(total_count), used_time=results_json['time'], Pass=str(success_count), error=str(error_count), rate='{}%'.format(str(success_rate)), url=url ) return ret if __name__ == "__main__": report_dir_name = str(int(time.time())) url = "http://60.205.217.8:5004/pro_mall/reports/{}".format(report_dir_name) content = Result.get_test_result( report_dir_name, "Pro H5商城API自動化測試執行", url, "H5商城自動化Pro.jmx" ) #WX.send_message(content, "3d38fe6b-c49f-46d6-8b17-9d92b9d5a143")
4.jmeter設置
合勾上:
輸出xml存儲位置:這里是腳本運行時指的變量存儲的位置resultsfile;
這個是利用了jmeter本身命令行輸出參數-J:jmeter -n -t {} -Jresultsfile={}
【結果樣式】