pytest 实战

目的是使用 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
效果预览:

Pasted image 20230225014023

删除检查列和link列,新增描述列

跟前面删除 link 列一样,使用 pytest_html_results_table_headerpytest_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__)

自动折叠

自动折叠通过和跳过,之前翻译漏了这部分导致自动折叠会有问题的。

hanxi/pytest-html-cn@5055c5b

隐藏不想要的复选框

比如错误,预期失败,预期通过我暂时用不上,就屏蔽了。

hanxi/pytest-html-cn@4c85de9

修改详情

使用 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)

最后

工具地址: https://github.com/hanxi/version-check-tool

点击进入评论 ...