pytest文档35-Hooks函数之统计测试结果(pytest_terminal_summary)

前言

用例执行完成后,我们希望能获取到执行的结果,这样方便我们快速统计用例的执行情况。

也可以把获取到的结果当成总结报告,发邮件的时候可以先统计测试结果,再加上html的报告。

pytest_terminal_summary

关于TerminalReporter类可以在_pytest.terminal中查看到

python 复制代码
from _pytest import terminal

pytest_terminal_summary(terminalreporter, exitstatus, config)
最后的结果汇总,可以拿到所有的执行结果
参数:
- terminalreporter (_pytest.terminal.TerminalReporter) -- 内部使用的终端测试报告对象
- exitstatus (int) -- 返回给操作系统的返回码
- config(_pytest.config.Config) - pytest config对象

TerminalReporter部分代码

python 复制代码
class TerminalReporter(object):
    def __init__(self, config, file=None):
        import _pytest.config

        self.config = config
        self._numcollected = 0
        self._session = None
        self._showfspath = None

        self.stats = {}
        self.startdir = config.invocation_dir


    def report_collect(self, final=False):
        if self.config.option.verbose < 0:
            return

        if not final:
            # Only write "collecting" report every 0.5s.
            t = time.time()
            if (
                self._collect_report_last_write is not None
                and self._collect_report_last_write > t - REPORT_COLLECTING_RESOLUTION
            ):
                return
            self._collect_report_last_write = t

        errors = len(self.stats.get("error", []))
        skipped = len(self.stats.get("skipped", []))
        deselected = len(self.stats.get("deselected", []))
        selected = self._numcollected - errors - skipped - deselected
        if final:
            line = "collected "
        else:
            line = "collecting "
        line += (
            str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s")
        )
        if errors:
            line += " / %d errors" % errors
        if deselected:
            line += " / %d deselected" % deselected
        if skipped:
            line += " / %d skipped" % skipped
        if self._numcollected > selected > 0:
            line += " / %d selected" % selected
        if self.isatty:
            self.rewrite(line, bold=True, erase=True)
            if final:
                self.write("\n")
        else:
            self.write_line(line)

案例参考

先在test_a.py写几个用例

python 复制代码
# test_a.py
import pytest
# 上海-悠悠

def test_1():
    print("测试用例1111")
    assert 1 == 1


@pytest.mark.skip("跳过")
def test_2():
    print("测试用例22222")
    assert 1 == 1


def test_3():
    print("测试用例3333")


def test_4():
    print("测试用例44444444")
    assert 1 == 2

test_b.py用例参考

python 复制代码
# test_b.py
import time
# 上海-悠悠

def test_5():
    print("测试用例55555555")
    time.sleep(3)


def test_6():
    print("测试用例66666666")
    time.sleep(3)
    assert 1 == 2

于是在conftest.py中写个 pytest_terminal_summary 函数收集测试结果

python 复制代码
import time
from _pytest import terminal
# 上海-悠悠


def pytest_terminal_summary(terminalreporter, exitstatus, config):
    '''收集测试结果'''
    print(terminalreporter.stats)
    print("total:", terminalreporter._numcollected)
    print('passed:', len(terminalreporter.stats.get('passed', [])))
    print('failed:', len(terminalreporter.stats.get('failed', [])))
    print('error:', len(terminalreporter.stats.get('error', [])))
    print('skipped:', len(terminalreporter.stats.get('skipped', [])))
    # terminalreporter._sessionstarttime 会话开始时间
    duration = time.time() - terminalreporter._sessionstarttime
    print('total times:', duration, 'seconds')

运行结果

python 复制代码
D:\soft\pytest_xuexi_demo>pytest
============================= test session starts =============================
platform win32 -- Python 3.6.0, pytest-4.5.0, py-1.5.4, pluggy-0.13.1
rootdir: D:\soft\pytest_xuexi_demo
plugins: allure-pytest-2.8.6, PyTestReport-0.1.9.3, forked-0.2, html-1.19.0, metadata-1.7.0, repeat-0.7.0, rerunfailures-8.0, xdist-1.23.2
collected 6 items

test_a.py .s.F                                                           [ 66%]
test_b.py .F                                                             [100%]

================================== FAILURES ===================================
___________________________________ test_4 ____________________________________

    def test_4():
        print("测试用例44444444")
>       assert 1==2
E       assert 1 == 2

test_a.py:21: AssertionError
---------------------------- Captured stdout call -----------------------------
测试用例44444444
___________________________________ test_6 ____________________________________

    def test_6():
        print("测试用例66666666")
        time.sleep(3)
>       assert 1 == 2
E       assert 1 == 2

test_b.py:18: AssertionError
---------------------------- Captured stdout call -----------------------------
测试用例66666666
{'': [<TestReport 'test_a.py::test_1' when='setup' outcome='passed'>, <TestReport 'test_a.py::test_1' when='teardown' outcome='passed'>, <TestReport 'test_a.py::test_2' when='teardown' outcome='passed'>, <TestReport 'test_a.py::test_3' when='setup' outcome='passed'>, <TestReport 'test_a.py::test_3' when='teardown' outcome='passed'>, <TestReport 'test_a.py::test_4' when='setup' outcome='passed'>, <TestReport 'test_a.py::test_4' when='teardown' outcome='passed'>, <TestReport 'test_b.py::test_5' when='setup' outcome='passed'>, <TestReport 'test_b.py::test_5' when='teardown' outcome='passed'>, <TestReport 'test_b.py::test_6' when='setup' outcome='passed'>, <TestReport 'test_b.py::test_6' when='teardown' outcome='passed'>], 'passed': [<TestReport 'test_a.py::test_1' when='call' outcome='passed'>, <TestReport 'test_a.py::test_3' when='call' outcome='passed'>, <TestReport 'test_b.py::test_5' when='call' outcome='passed'>], 'skipped': [<TestReport 'test_a.py::test_2' when='setup' outcome='skipped'>], 'failed': [<TestReport 'test_a.py::test_4' when='call' outcome='failed'>, <TestReport 'test_b.py::test_6' when='call' outcome='failed'>]}
total: 6
passed: 3
failed: 2
error: 0
skipped: 1
total times: 6.150860786437988 seconds
================ 2 failed, 3 passed, 1 skipped in 6.15 seconds ================

setup和teardown异常情况

如果setup出现异常,test_b.py代码修改下

python 复制代码
# test_b.py
import time
import pytest
# 上海-悠悠


@pytest.fixture(scope="function")
def setup_demo():
    raise TypeError("ERROR!")


def test_5(setup_demo):
    print("测试用例55555555")
    time.sleep(3)


def test_6():
    print("测试用例66666666")
    time.sleep(3)
    assert 1 == 2

重新运行用例,结果如下

复制代码
total: 6
passed: 2
failed: 2
error: 1
skipped: 1
成功率:33.33%
total times: 3.1817877292633057 seconds
=========== 2 failed, 2 passed, 1 skipped, 1 error in 3.18 seconds ============

此时统计结果没什么问题,接下来看teardown异常情况

复制代码
# test_b.py
import time
import pytest
# 上海-悠悠


@pytest.fixture(scope="function")
def setup_demo():
    yield 
    raise TypeError("ERROR!")


def test_5(setup_demo):
    print("测试用例55555555")
    time.sleep(3)


def test_6():
    print("测试用例66666666")
    time.sleep(3)
    assert 1 == 2

运行结果

复制代码
{'': [<TestReport 'test_a.py::test_1' when='setup' outcome='passed'>, <TestReport 'test_a.py::test_1' when='teardown' outcome='passed'>, <TestReport 'test_a.py::test_2' when='teardown' outcome='passed'>, <TestReport 'test_a.py::test_3' when='setup' outcome='passed'>, <TestReport 'test_a.py::test_3' when='teardown' outcome='passed'>, <TestReport 'test_a.py::test_4' when='setup' outcome='passed'>, <TestReport 'test_a.py::test_4' when='teardown' outcome='passed'>, <TestReport 'test_b.py::test_5' when='setup' outcome='passed'>, <TestReport 'test_b.py::test_6' when='setup' outcome='passed'>, <TestReport 'test_b.py::test_6' when='teardown' outcome='passed'>], 'passed': [<TestReport 'test_a.py::test_1' when='call' outcome='passed'>, <TestReport 'test_a.py::test_3' when='call' outcome='passed'>, <TestReport 'test_b.py::test_5' when='call' outcome='passed'>], 'skipped': [<TestReport 'test_a.py::test_2' when='setup' outcome='skipped'>], 'failed': [<TestReport 'test_a.py::test_4' when='call' outcome='failed'>, <TestReport 'test_b.py::test_6' when='call' outcome='failed'>], 'error': [<TestReport 'test_b.py::test_5' when='teardown' outcome='failed'>]}
total: 6
passed: 3
failed: 2
error: 1
skipped: 1
成功率:50.00%
total times: 6.18759298324585 seconds
=========== 2 failed, 3 passed, 1 skipped, 1 error in 6.19 seconds ============

这个时候总用例是6,但是2 failed, 3 passed, 1 skipped, 1 error加起来的个数是7,这个为什么?

从 terminalreporter.stats 可以看出 passed 里面 when='call' 时候统计了一次 test_5 用例

复制代码
<TestReport 'test_b.py::test_5' when='call' outcome='passed'>

error 里面 when='teardown' 又统计了一次 test_5 用例

复制代码
'error': [<TestReport 'test_b.py::test_5' when='teardown' outcome='failed'>]

when='teardown' 是测试用例的后置操作,一般用于数据的清理,报错了的话不影响测试用例的执行结果,所以可以忽略掉

修改后的最终代码如下

复制代码
import time
from _pytest import terminal
# 上海-悠悠


def pytest_terminal_summary(terminalreporter, exitstatus, config):
    '''收集测试结果'''
    # print(terminalreporter.stats)
    print("total:", terminalreporter._numcollected)
    print('passed:', len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown']))
    print('failed:', len([i for i in terminalreporter.stats.get('failed', []) if i.when != 'teardown']))
    print('error:', len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown']))
    print('skipped:', len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown']))
    print('成功率:%.2f' % (len(terminalreporter.stats.get('passed', []))/terminalreporter._numcollected*100)+'%')

    # terminalreporter._sessionstarttime 会话开始时间
    duration = time.time() - terminalreporter._sessionstarttime
    print('total times:', duration, 'seconds')

运行后的结果

复制代码
total: 6
passed: 3
failed: 2
error: 0
skipped: 1
成功率:50.00%
total times: 6.20133113861084 seconds
=========== 2 failed, 3 passed, 1 skipped, 1 error in 6.20 seconds ============

拿到测试结果

很多小伙伴问到如何拿到测试结果,这里我把测试结果保存为txt文件,你们也可以保存json文件

python 复制代码
import time
from _pytest import terminal
# 111
# blog地址 https://www.cnblogs.com/yoyoketang/


def pytest_terminal_summary(terminalreporter, exitstatus, config):
    '''收集测试结果'''
    # print(terminalreporter.stats)
    total = terminalreporter._numcollected
    passed= len([i for i in terminalreporter.stats.get('passed', []) if i.when != 'teardown'])
    failed=len([i for i in terminalreporter.stats.get('failed', []) if i.when != 'teardown'])
    error=len([i for i in terminalreporter.stats.get('error', []) if i.when != 'teardown'])
    skipped=len([i for i in terminalreporter.stats.get('skipped', []) if i.when != 'teardown'])
    successful = len(terminalreporter.stats.get('passed', []))/terminalreporter._numcollected*100
    # terminalreporter._sessionstarttime 会话开始时间
    duration = time.time() - terminalreporter._sessionstarttime
    print('total times: %.2f' % duration, 'seconds')

    with open("result.txt", "w") as fp:
        fp.write("TOTAL=%s" % total+"\n")
        fp.write("PASSED=%s" % passed+"\n")
        fp.write("FAILED=%s" % failed+"\n")
        fp.write("ERROR=%s" % error+"\n")
        fp.write("SKIPPED=%s" % skipped+"\n")
        fp.write("SUCCESSFUL=%.2f%%" % successful+"\n")
        fp.write("TOTAL_TIMES=%.2fs" % duration)

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!

相关推荐
不念霉运2 小时前
为什么传统 Bug 追踪系统正在被抛弃?
软件测试·安全·gitee·开源·bug·devsecops
鹏码纵横5 小时前
已解决:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver 异常的正确解决方法,亲测有效!!!
java·python·mysql
仙人掌_lz5 小时前
Qwen-3 微调实战:用 Python 和 Unsloth 打造专属 AI 模型
人工智能·python·ai·lora·llm·微调·qwen3
猎人everest6 小时前
快速搭建运行Django第一个应用—投票
后端·python·django
猎人everest6 小时前
Django的HelloWorld程序
开发语言·python·django
chusheng18407 小时前
2025最新版!Windows Python3 超详细安装图文教程(支持 Python3 全版本)
windows·python·python3下载·python 安装教程·python3 安装教程
别勉.7 小时前
Python Day50
开发语言·python
自由鬼7 小时前
企业架构框架深入解析:TOGAF、Zachman Framework、FEAF与Gartner EA Framework
程序人生·架构
xiaohanbao097 小时前
day54 python对抗生成网络
网络·python·深度学习·学习
爬虫程序猿7 小时前
利用 Python 爬虫按关键字搜索 1688 商品
开发语言·爬虫·python