目的是使用 pytest 实现一套静态检查工具,采用 pytest 是因为一套完整的静态检查工具的产出的检查报告跟 pytest-html 产出的检查报告非常像,而且检查工具的每个步骤就相当于一个测试用例,并且测试用例直接可以有前后依赖关系。正式利用了 pytest 提供的这一套基础设施,可以比较方便的造出这个静态检查工具。
工程目录结构如下:
.
├── gen_report.sh
├── install.sh
├── pytest-html
│ ├── codecov.yml
│ ├── docs
│ ├── Gruntfile.js
│ ├── LICENSE
│ ├── package.json
│ ├── Pipfile
│ ├── pyproject.toml
│ ├── README.rst
│ ├── setup.cfg
│ ├── setup.py
│ ├── src
│ ├── testing
│ └── tox.ini
├── reports
│ └── report.html
└── src
├── contest.py
├── init_test.py
└── __pycache__
gen_report.sh 用于生成测试报告
now=$(date +"%Y-%m-%d-%H-%M-%S")
#pytest --html=reports/report-$now.html --self-contained-html src
pytest --html=reports/report.html --self-contained-html src
正式环境运行时,给生成的报告文件加上时间。为了方便测试,测试环境就不加时间了。
install.sh 用于安装 pytest-html 包,由于需要修改,所以直接把包引入工程目录了。
cd pytest-html
pip install -e .
reports 目录用于存放生成的报告文件, src 目录用于存放测试用例。
接下来会根据制作静态检查工具所需的环境,对 pytest 以及 pytest-html 进行配置以及改造。
获取命令行参数
修改 src/contest.py 文件,使用 pytest_addoption
接口来新增参数:
import pytest
def pytest_addoption(parser):
parser.addoption(
"--branch", action="store", default="master", help="branch: 分支名"
)
@pytest.fixture()
def branch(request):
return request.config.getoption("--branch")
并使用 pytest.fixture
装饰器新增一个 branch
参数给测试用例使用。
配置测试用例之间的依赖关系
想要某些测试用例依赖某些测试用例成功执行,可以使用 pytest.mark.skipif
实现,在满足第一个参数为 True
时跳过用例。
新键文件 src/init_test.py
,pytest 是会自动执行以 test 开始或者结尾的 py 文件的。
首先定义一个全局变量 g_init_succ
用来标记是否初始化完毕,然后用这个全局变量创建一个装饰器 checkinit = pytest.mark.skipif(not g_init_succ, reason='初始化失败')
。
接下来在第 1 个测试用例中,如果 init()
执行成功,则将 g_init_succ
设置为 True
,在第 2 个测试用例就可以使用前面创建的装饰器 checkinit
了。达到的效果就是 init()
执行成功,才执行第 2 个测试用例 test_need_init
,如果 init()
执行失败就不执行第 2 个测试用例了。
import pytest
g_init_succ = False
checkinit = pytest.mark.skipif(not g_init_succ, reason='初始化失败')
# 初始化环境
def init():
#return True
return False
def test_init(branch):
global g_init_succ
print("in test_init")
# branch 就是前面 fixture 装饰器创建的参数
print("branch:", branch)
if init():
g_init_succ = True
else:
assert False, "初始化环境失败"
print("init_succ", g_init_succ)
@checkinit
def test_need_init():
print("g_init_succ:", g_init_succ)
print("succ")
pass
变量比较多的情况怎么办?
可以弄个 env 表,修改 conftest.py
文件:
@pytest.fixture()
def env(request):
return {
'branch': request.config.getoption("--branch"),
'svn_root': "http://test.com",
'svn_user': "user",
'svn_passwd': "passwd",
}
修改 Environment 里显示的内容
修改 conftest.py
文件:
def pytest_configure(config):
config._metadata.clear()
config._metadata['分支'] = config.getoption('--branch')
删除 link 列
修改 conftest.py
文件:
def pytest_html_results_table_header(cells):
cells.pop(-1) # 删除link列
def pytest_html_results_table_row(report, cells):
cells.pop(-1) # 删除link列
修改标题
def pytest_html_report_title(report):
report.title = "版本静态检查报告"
翻译成中文
参考这篇文章: https://www.cnblogs.com/linuxchao/p/linuxchao-pytest-html.html
翻译成果: https://github.com/hanxi/pytest-html-cn
效果预览:
删除检查列和link列,新增描述列
跟前面删除 link 列一样,使用 pytest_html_results_table_header
和 pytest_html_results_table_row
接口实现。
并通过 pytest_runtest_makereport
接口新增列所需的数据。
from py.xml import html
def pytest_html_results_table_header(cells):
cells.pop(1) # 删除检查列
cells.pop(-1) # 删除link列
cells.insert(1, html.th("描述"))
def pytest_html_results_table_row(report, cells):
cells.pop(1) # 删除检查列
cells.pop(-1) # 删除link列
cells.insert(1, html.td(report.description))
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__)
自动折叠
自动折叠通过和跳过,之前翻译漏了这部分导致自动折叠会有问题的。
隐藏不想要的复选框
比如错误,预期失败,预期通过我暂时用不上,就屏蔽了。
修改详情
使用 pytest_html_results_table_html
接口修改详情,先清空 data ,然后重新构造 data 。
比如 skipped 的情况下,默认打印格式是这样的:
('/home/hanxi/work/pytest-demo/src/init_test.py', 21, 'Skipped: 初始化失败')
我只想打印跳过的原因,就删除首位括号,然后切割字符串,只打印最后一个元素。
想要隐藏 failed 的时候输出的报错堆栈,就不输出堆栈数据。
从 report.sections 获取 stdout 数据,然后又不输出标题。
这些都是从 def _populate_html_log_div
函数的实现里修改过来的:
def pytest_html_results_table_html(report, data):
del data[:]
log = html.div(class_="log")
if report.skipped and report.longrepr:
arr = report.longreprtext.strip("()").split(",")
skip_msg = arr[len(arr)-1]
log.append(html.span(raw(escape(skip_msg))))
elif report.failed and report.longrepr:
pass
elif report.longrepr:
text = report.longreprtext or report.full_text
for line in text.splitlines():
separator = line.startswith("_ " * 10)
if separator:
log.append(line[:80])
else:
exception = line.startswith("E ")
if exception:
log.append(html.span(raw(escape(line)), class_="error"))
else:
log.append(raw(escape(line)))
log.append(html.br())
for section in report.sections:
header, content = map(escape, section)
converter = Ansi2HTMLConverter(
inline=False, escaped=False,
)
content = converter.convert(content, full=False)
log.append(raw(content))
log.append(html.br())
data.append(log)