总览
在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.py
的stringify_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.py
的render_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包下,可以直接访问系统中的文件。
网友评论