【引言】
由于要使用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】
【生成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={}
【结果样式】