软件测试

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

2021-03-11  本文已影响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中的内置函数,可以对变量进行相应的处理,常用的过滤器有:

过滤器 作用
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支持两种加载方式:

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)   # 渲染

其中:

3. FileSystemLoader

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

上一篇下一篇

猜你喜欢

热点阅读