美文网首页软件测试
HttpRunner2.x源码分析——生成报告

HttpRunner2.x源码分析——生成报告

作者: 卫青臣 | 来源:发表于2021-03-11 17:36 被阅读0次

    总览

    api.py中的run_tests函数中,httprunner执行测试用例并生成报告

    def run_tests(self, tests_mapping):
        """
        此处省略源码中的部分无关内容
        """
        results = self._run_suite(test_suite)
        
        # 汇总测试结果
        self.exception_stage = "aggregate results"
        self._summary = self._aggregate(results)
    
        self.exception_stage = "generate html report"
        # 串化测试结果汇总
        report.stringify_summary(self._summary)
    
        if self.save_tests:
            utils.dump_logs(self._summary, project_mapping, "summary")
            
        # 生成html测试报告
        report_path = report.render_html_report(
            self._summary,
            self.report_template,
            self.report_dir
        )
    
        return report_path
    

    生成测试结果汇总

    api.py的_aggregate函数:

    def _aggregate(self, tests_results):
        """ 
        汇总测试报告。test_results参数是[(testcase, result), (testcase, result), ...]形式的list
        方法中的repory.xxx(xxx)是调用report.py中的函数
        """
        # 报告格式
        summary = {
            "success": True,
            "stat": {
                "testcases": {
                    "total": len(tests_results),
                    "success": 0,
                    "fail": 0
                },
                "teststeps": {}
            },
            "time": {},
            # 获取httprunner版本、python版本和运行平台
            "platform": report.get_platform(),
            "details": []
        }
    
        # 遍历每个测试的结果
        for tests_result in tests_results:
            testcase, result = tests_result
            '''
            get_summary函数返回如下形式的dict:
            {
                "success": True,
                "stat": {},
                "time": {},
                "records": []
            }
            '''
            testcase_summary = report.get_summary(result)
    
            if testcase_summary["success"]:
                summary["stat"]["testcases"]["success"] += 1
            else:
                summary["stat"]["testcases"]["fail"] += 1
    
            # 使用&=,只要有一个用例执行不通过,那么本次测试结果为失败
            summary["success"] &= testcase_summary["success"]
            testcase_summary["name"] = testcase.config.get("name")
            # 获取和打印用例的入参和导出变量(用例中export的字段或output的字段)
            testcase_summary["in_out"] = utils.get_testcase_io(testcase)
    
            report.aggregate_stat(summary["stat"]["teststeps"], testcase_summary["stat"])
            report.aggregate_stat(summary["time"], testcase_summary["time"])
    
            summary["details"].append(testcase_summary)
    
        return summary
    

    串化测试结果汇总

    得到测试结果汇总后,通过report.pystringify_summary函数将测试汇总串化,以便转储json文件并生成html报告。

    def stringify_summary(summary):
        """ stringify summary, in order to dump json file and generate html report.
        """
        for index, suite_summary in enumerate(summary["details"]):
    
            if not suite_summary.get("name"):
                # 如果测试集没有命名,以第一个用例名作为集合名
                suite_summary["name"] = "testcase {}".format(index)
    
            for record in suite_summary.get("records"):
                meta_datas = record['meta_datas']
                # 串化元数据
                __stringify_meta_datas(meta_datas)
                meta_datas_expanded = []
                __expand_meta_datas(meta_datas, meta_datas_expanded)
                record["meta_datas_expanded"] = meta_datas_expanded
                record["response_time"] = __get_total_response_time(meta_datas_expanded)
    
    def __stringify_meta_datas(meta_datas):
        if isinstance(meta_datas, list):
            for _meta_data in meta_datas:
                __stringify_meta_datas(_meta_data)
        elif isinstance(meta_datas, dict):
            data_list = meta_datas["data"]
            for data in data_list:
                # 串化请求数据
                __stringify_request(data["request"])
                # 串化响应数据
                __stringify_response(data["response"])
    
    def __stringify_request(request_data):
        for key, value in request_data.items():
            if isinstance(value, list):
                # 将list类型的数据转为格式化json字符串
                value = json.dumps(value, indent=2, ensure_ascii=False)
            elif isinstance(value, bytes):
                try:
                    encoding = "utf-8"
                    # 将字节转为utf-8解码后的字符串
                    value = escape(value.decode(encoding))
                except UnicodeDecodeError:
                    pass
            elif not isinstance(value, (basestring, numeric_types, Iterable)):
                # class instance, e.g. MultipartEncoder()
                # 将对象转为对象的规范字符串表示形式
                value = repr(value)
            elif isinstance(value, requests.cookies.RequestsCookieJar):
                # 将cookies转为dict类型数据集
                value = value.get_dict()
            request_data[key] = value
    
    
    def __stringify_response(response_data):
        for key, value in response_data.items():
            if isinstance(value, list):
                # 将list类型的数据转为格式化json字符串
                value = json.dumps(value, indent=2, ensure_ascii=False)
            elif isinstance(value, bytes):
                try:
                    encoding = response_data.get("encoding")
                    if not encoding or encoding == "None":
                        encoding = "utf-8"
                    if key == "content" and "image" in response_data["content_type"]:
                        # display image
                        value = "data:{};base64,{}".format(
                            response_data["content_type"], 
                            b64encode(value).decode(encoding)
                        )
                    else:
                        # 将字节转为utf-8解码后的字符串
                        value = escape(value.decode(encoding))
                except UnicodeDecodeError:
                    pass
            elif not isinstance(value, (basestring, numeric_types, Iterable)):
                # class instance, e.g. MultipartEncoder()
                # 将对象转为对象的规范字符串表示形式
                value = repr(value)
            elif isinstance(value, requests.cookies.RequestsCookieJar):
                # 将cookies转为dict类型数据集
                value = value.get_dict()
            response_data[key] = value
    

    生成html报告

    串化测试汇总后,通过report.pyrender_html_report函数生成html报告

    def render_html_report(summary, report_template=None, report_dir=None):
        # html报告模板文件路径,如果不传参数,默认用内置模板(./templates/report_template.html)
        if not report_template:
            report_template = os.path.join(
                os.path.abspath(os.path.dirname(__file__)),
                "templates",
                "report_template.html"
            )
            logger.log_debug("No html report template specified, use default.")
        else:
            logger.log_info("render with html report template: {}".format(report_template))
    
        logger.log_info("Start to render Html report ...")
        # 报告输出路径,如果不传参数,默认在当前工作目录创建reports目录作为输出目录
        report_dir = report_dir or os.path.join(os.getcwd(), "reports")
        if not os.path.isdir(report_dir):
            os.makedirs(report_dir)
    
        start_at_timestamp = int(summary["time"]["start_at"])
        summary["time"]["start_datetime"] = datetime.fromtimestamp(start_at_timestamp).strftime('%Y-%m-%d %H:%M:%S')
        # 默认使用测试开始时间戳作为报告文件名
        report_path = os.path.join(report_dir, "{}.html".format(start_at_timestamp))
    
        with io.open(report_template, "r", encoding='utf-8') as fp_r:
            # 打开模板文件,读取模板内容
            template_content = fp_r.read()
            with io.open(report_path, 'w', encoding='utf-8') as fp_w:
                # 按照模板和串化后的测试汇总,生成测试报告文件内容
                rendered_content = Template(
                    template_content,
                    extensions=["jinja2.ext.loopcontrols"]
                ).render(summary)
                # 按上面的文件名创建报告文件并写入测试报告内容
                fp_w.write(rendered_content)
    
        logger.log_info("Generated Html report: {}".format(report_path))
    
        return report_path
    

    jinja2模板语法

    httprunner使用jinja2生成测试报告,下文简单介绍jinja2的用法,想要更深入了解请查看:

    语法

    在jinja2中有3中语法:

    • 控制结构{% %}
    • 控制取值{{ }},相当于一种特殊的占位符,在jinja2渲染时会把这些占位符进行填充
    • 注释{# #}

    过滤器

    过滤器相当于jinja2中的内置函数,可以对变量进行相应的处理,常用的过滤器有:

    过滤器 作用
    safe 渲染时值不转义
    capitialize 首字母大小其余小写
    lower 转为小写
    upper 转为大写
    title 每个单词首字母大写
    trim 去除首尾空格
    striptags 渲染前删除所有html标签
    join 将多个值拼接为字符串
    replace 替换字符串的值
    round 默认四舍五入,可由参数控制
    int 将值转为整型

    过滤器用法:在变量后使用管道|调用,可链式调用

    {{ 'abc' | capitialize }}
    # 渲染为 Abc
    {{ 'hello world' | title }}
    # 渲染为 Hello World
    {{ 'hello world' | replace('world', 'python') | title }}
    # 渲染为 Hello Python
    

    for循环

    for循环由于迭代python中的列表和字典

    1、迭代list
    <ul>
        {% for user in users %}
        <li>{{ user.username | title }}</li>
        {% endfor %}
    </ul>
    
    2、迭代dict
    <dl>
        {% for key, value in my_dict.iteritems() %}
        <dt>{{ key }}</dt>
        <dd>{{ value}}</dd>
        {% endfor %}
    </dl>
    

    宏相当于jinja2的自定义函数,定义宏的关键字是macro,后接宏的名称和参数

    {% macro input(name,age=18) %}   # 参数age的默认值为18
    <input type='text' name="{{ name }}" value="{{ age }}" >
    {% endmacro %}
    

    调用宏

    <p>{{ input('daxin') }} </p>
    <p>{{ input('daxin',age=20) }} </p>
    

    继承和super函数

    jinja2中最强大的部分就是模板继承。模板继承允许我们创建一个基本(骨架)文件,其他文件从该骨架文件继承,然后针对自己需要的地方进行修改。

    jinja2的骨架文件中,利用block关键字表示其包涵的内容可以进行修改。

    以下面的骨架文件base.html为例:

    {% extend "base.html" %}       # 继承base.html文件
    {% block title %} Dachenzi {% endblock %}   # 定制title部分的内容
    {% block head %}
        {{  super()  }}        # 用于获取原有的信息
        <style type='text/css'>
        .important { color: #FFFFFF }
        </style>
    {% endblock %}   
    # 其他不修改的原封不动的继承
    

    渲染

    jinja2模块中有一个名为Enviroment的类,这个类的实例用于存储配置和全局对象,然后从文件系统或其他位置中加载模板。

    1、基本用法

    大多数应用都在初始化的时候撞见一个Environment对象,并用它加载模板。Environment支持两种加载方式:

    • PackageLoader:包加载器
    • FileSystemLoader:文件系统加载器
    2、PackageLoader

    使用包加载器来加载文档的最简单的方式如下:

    from jinja2 import PackageLoader,Environment
    env = Environment(loader=PackageLoader('python_project','templates'))    # 创建一个包加载器对象
     
    template = env.get_template('bast.html')    # 获取一个模板文件
    template.render(name='daxin',age=18)   # 渲染
    

    其中:

    • PackageLoader()的两个参数为:python包的名称,以及模板目录名称。
    • get_template():获取模板目录下的某个具体文件。
    • render():接受变量,对模板进行渲染
    3. FileSystemLoader

    文件系统加载器,不需要模板文件存在某个Python包下,可以直接访问系统中的文件。

    相关文章

      网友评论

        本文标题:HttpRunner2.x源码分析——生成报告

        本文链接:https://www.haomeiwen.com/subject/sbroqltx.html