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

总览

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包下,可以直接访问系统中的文件。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容

  • 模板: 在之前的章节中,视图函数只是直接返回文本,而在实际生产环境中其实很少这样用,因为实际的页面大多是带有样式和...
    帅气的Lucky阅读 1,787评论 0 1
  • 前言 来啦老铁! 笔者近期在工作中遇到将测试数据可视化的需求,且在Python语言背景下,当时借用的是团队既有经验...
    狄仁杰666阅读 8,690评论 0 4
  • 第三章 模板(Templates) 编写易于维护的程序的要点在于书写干净、良好结构的代码。你以前所见的代码都过于...
    易木成华阅读 1,134评论 0 2
  • 模版讲解: {{ ... }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。 {%...
    林博伦阅读 1,363评论 0 0
  • 一:渲染模版 要渲染一个模板,通过render_template方法即可。 渲染模版时有两种传递参数的方式:用 v...
    忘了呼吸的那只猫阅读 730评论 0 1