基於Jmeter的接口測試報告


【引言】

由於要使用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)
report.py

 

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()
    )
sax_xml.py

 

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")
run_jmeter.py

 

4.jmeter設置

 

 

 

 合勾上:

 

 輸出xml存儲位置:這里是腳本運行時指的變量存儲的位置resultsfile;

這個是利用了jmeter本身命令行輸出參數-J:jmeter -n -t {}  -Jresultsfile={}

 

 

 

 

【結果樣式】

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM