首页 > 技术文章 > (详细)Pytest框架的常用参数,第三方插件,fixture,参数化,hook函数介绍

shanshan-test 2021-08-12 15:29 原文

pytest 的简介与安装

pytest 的特点

  • 简单灵活,容易上手,文档丰富
  • 支持参数化
  • 能够支持简单的单元测试和复杂的功能测试,还可以用来做selenium/appnium等自动化测试、接口自动化测试(pytest+requests)
  • pytest具有很多第三方插件,并且可以自定义扩展,比较好用的如pytest-selenium(集成selenium)、pytest-html(完美html测试报告生成)、pytest-rerunfailures(失败case重复执行)、pytest-xdist(多CPU分发)等
  • 测试用例的skip和xfail处理
  • 支持运行unittest编写的测试case
  • 可以很好的和jenkins集成
  • 支持 allure 报告

pytest 的安装与验证

  • pytest 的安装
      pip install pytest
  • 验证 pytest 的安装
      pytest --version

pytest 与 unittest 对比

unitest pytest
测试用例 需继承unitest.TestCase类 无需继承,支持面向函数,面向对象的测试
参数化 参数化需依赖第三方插件 bdd 参数化支持两种方式?
插件 没有插件 有很丰富的插件
失败重试 不支持失败重试 支持失败重试
断言 断言使用unittest 断言方法 使用 python 自带 assert 方法
报告 报告使用 HTMLTestRunner 报告支持 html,allure
测试用例收集 收集用例使用 discover 方法 支持按文件名,类名,测试用例名去运行
扩展性 扩展性不强 扩展性强

pytest 简单使用

  • 使用 pycharm 普通运行方式输出
/usr/local/bin/python3.7 /Users/mac/Desktop/download/UIAutoTest/DemoPytest/test_simple.py
============================= test session starts ==============================
platform darwin -- Python 3.7.8, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/mac/Desktop/download/UIAutoTest/DemoPytest
plugins: rerunfailures-9.1.1, allure-pytest-2.8.16
collecting ... ============================= test session starts ==============================
platform darwin -- Python 3.7.8, pytest-5.4.3, py-1.9.0, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
rootdir: /Users/mac/Desktop/download/UIAutoTest/DemoPytest
plugins: rerunfailures-9.1.1, allure-pytest-2.8.16
collecting ... collected 3 items

test_simple.py::test_001 PASSED
test_simple.py::TestDemo::test_002 PASSED
test_simple.py::TestDemo::test_003 FAILED

=================================== FAILURES ===================================
______________________________ TestDemo.test_003 _______________________________

self = <DemoPytest.test_simple.TestDemo object at 0x10abd6550>

    def test_003(self):
>       assert 3 == 4
E       assert 3 == 4
E         +3
E         -4

test_simple.py:18: AssertionError
=========================== short test summary info ============================
FAILED test_simple.py::TestDemo::test_003 - assert 3 == 4
========================= 1 failed, 2 passed in 0.10s ==========================
collected 3 items

test_simple.py::test_001 PASSED
test_simple.py::TestDemo::test_002 PASSED
test_simple.py::TestDemo::test_003 FAILED

=================================== FAILURES ===================================
______________________________ TestDemo.test_003 _______________________________

self = <DemoPytest.test_simple.TestDemo object at 0x10abe8f50>

    def test_003(self):
>       assert 3 == 4
E       assert 3 == 4
E         +3
E         -4

test_simple.py:18: AssertionError
=============================== warnings summary ===============================
/usr/local/lib/python3.7/site-packages/_pytest/compat.py:333
  /usr/local/lib/python3.7/site-packages/_pytest/compat.py:333: PytestDeprecationWarning: The TerminalReporter.writer attribute is deprecated, use TerminalReporter._tw instead at your own risk.
  See https://docs.pytest.org/en/latest/deprecations.html#terminalreporter-writer for more information.
    return getattr(object, name, default)

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=========================== short test summary info ============================
FAILED test_simple.py::TestDemo::test_003 - assert 3 == 4
==================== 1 failed, 2 passed, 1 warning in 0.19s ====================

Process finished with exit code 0

  • 使用 pytest 运行方式运行

  • pytest 方式运行会自动搜索测试用例,无法生成测试报告
  • 如何修改运行方式呢?

pytest 的常用命令行参数

  • 参数:-s(用于显示测试用例中的 print()信息)

  • 参数:-v(用于详细显示测试用例的执行过程)

  • 参数:-q(简化控制台输出,只显示整体测试结果)

  • 参数:-x(运行到失败的用例就停止)

  • 参数:--maxfail=x(出现 x 个失败用例既停止)

  • 参数:--collect-only(收集将要执行的用例,但不会执行用例)

  • 参数:-k(关键字过滤,可过滤文件名,类名,方法名/函数名)

  • 参数: -m (只运行带有对应标识的测试用例,需使用@pytest.mark.xxxx进行修饰,后面跟自己的标识,一般进行标记冒烟测试,系统测试,登录测试等)

  • 参数:--result-log=path(将执行结果保存到指定路径下)
# -*- conding:utf-8 -*-
# @Author : shanshan
# @File : test_simple.py

import pytest


class TestDemo():

    def test_001(self):
        assert 2 == 2

    def test_002(self):
        assert 2 == 3

    @pytest.mark.smoking
    def test_003(self):
        assert 3 == 3


pytest.main(['test_simple.py', '--result-log=/Users/mac/Desktop/download/UIAutoTest/DemoPytest/a.txt'])

  • 参数:--junit-xml=path (输出xml文件格式,在与jenkins做集成时使用)

  • 参数:--durations=num (显示执行最慢的测试用例,一般用于调优,注意如果要显示执行时间为 0 秒的用例时,要加上参数-vv)

  • 参数:-ff (先执行完上次失败的测试后,再执行上次正常的测试)

  • 参数: -lf (只运行上次运行失败的用例)

  • 参数:-l (用例运行失败时,打印用例中的局部变量)

pytest常用的第三方插件

失败重试

  • 安装:
pip install -U pytest-rerunfailures
  • 使用

html 报告

  • 安装:
pip3 install pytest-html
  • 使用

pytest控制函数执行顺序

  • 安装:
pip3 install pytest-ordering
  • 使用方法
1.标记于被测试函数,@pytest.mark.run(order=x)
2.根据order传入的参数来解决运行顺序
3.order值全为正数或全为负数时,运行顺序:值越小,优先级越高
4.正数和负数同时存在:正数优先级高
注:默认情况下,pytest是根据测试方法名由小到大执行的,可以通过第三方插件包改变其运行顺序。

多重校验

  • 安装:
pip3 install  pytest-assume
  • 使用:

用例依赖

  • 安装
pip install pytest-dependency
  • 说明:

  • 使用该插件可以标记一个testcase作为其他testcase的依赖,当依赖项执行失败时,那些依赖它的test将会被跳过。

  • 用 @pytest.mark.dependency()对所依赖的方法进行标记,使用@pytest.mark.dependency(depends=["test_name"])引用依赖,test_name可以是多个。

  • 使用:

分布式执行

  • 安装
pip3 install pytest-xdist
  • 说明:

  • 平常我们功能测试用例非常多时,比如有1千条用例,假设每个用例执行需要1分钟,如果单个测试人员执行需要1000分钟才能跑完
  • 当项目非常紧急时,会需要协调多个测试资源来把任务分成两部分,于是执行时间缩短一半,如果有10个小伙伴,那么执行时间就会变成十分之一,大大节省了测试时间
  • 为了节省项目测试时间,10个测试同时并行测试,这就是一种分布式场景
  • 分布式执行用例的原则
    • 用例之间是独立的,没有依赖关系,完全可以独立运行
    • 用例执行没有顺序要求,随机顺序都能正常执行
    • 每个用例都能重复运行,运行结果不会影响其他用例
  • 使用
pytest.main(['test_simple.py','-n=2'])
pytest.main(['test_simple.py','-n=auto'])
  • 注意:要根据自己电脑的 cpu 数指定,电脑不好肯定会直接让电脑卡死

重复执行用例

  • 安装
pip3 install pytest-repeat
  • 说明:

  • pytest-repeat是pytest的一个插件,用于重复执行单个用例,或多个测试用例,并指定重复次数

  • 使用:

  • --repeat-scope

  • function(默认)范围针对每个用例重复执行,再执行下一个用例

  • class 以class为用例集合单位,重复执行class里面的用例,再执行下一个

  • module 以模块为单位,重复执行模块里面的用例,再执行下一个

  • session 重复整个测试会话,即所有收集的测试执行一次,然后所有这些测试再次执行等等

  • 注:--repeat-scope类似于pytest fixture的scope参数,执行完 scope 的参数单位后才会去执行下一个单位的。


  • 如果要在代码中标记要重复多次的测试,可以使用@pytest.mark.repeat(count)装饰器

  • 重复测试直到失败停止

pytest.main(['test_simple.py::TestDemo::test_001','-x','--count=10000'])
重复运行test_001用例 10000 次,直到失败时停止

常用插件地址

pytest 的 fixture

fixture 相比setup和teardown的优势

  • 命名方式灵活,不局限于setup和teardown这几个命名
  • conftest 可以配置数据共享,
  • scope支持四个级别参数 "function" (默认), "class", "module" or "session"
  • 可通过 yield语句为测试函数区分前置条件和后置清理

使用1,在当前测试用例的文件中使用

使用 2,使用conftest.py 文件

  • conftest.py配置需要注意以下点:
    • conftest.py配置脚本名称是固定的,不能改名称
    • conftest.py与运行的用例要在同一个pakage下,并且有__init__.py文件
    • 不需要import导入 conftest.py,pytest用例会自动查找


多个 fixture 之间的相互调用

  • 建议多使用 fixture,很多测试用例需要前置条件,后置清理时,它是你的不二之选

pytest 的参数化

(1)pytest.mark.parametrize装饰器

  • 实质上就是数据驱动测试 DDT

  • 当参数化应用在类上时,则此时类的所有测试方法都将使用参数化中的变量

  • 在使用参数化的过程中也可以使用标记,比如标记为fail或者skip

(2)使用fixture进行参数化

  • (3)参数化之JSON 文件
  • 参数化之 Excel文件,Yaml文件道理都是一样的,读到的数据是一个列表,列表里面嵌套列表,字典,元祖都可以,但是要和参数名称一一对应起来哦
# 创建一个 json 文件
{
  "login_param":
          [
            {
              "username": "aaa",
              "password": "aaa"
            },
            {
              "username": "bbb",
              "password": "bbb"
            },
            {
              "username": "ccc",
              "password": "ccc"
            },
            {
              "username": "ddd",
              "password": "ddd1"
            }
          ]
}



# conftest 文件代码
def read_login_json():
    with open("login.json", 'r', encoding="UTF-8") as f:
        data = json.load(f)['login_param']
    return data


# json参数化
@pytest.fixture(params=read_login_json())
def login_params(request):  # 参数 request固定写法
    return request.param  # requests.param 固定写法


# test_simple.py 文件
class TestDemo(object):

    def test_002(self, login_params):
        assert login_params["username"] == login_params["password"]


pytest.main(['test_simple.py::TestDemo',
             '-v', ])

pytest 的 hook 函数

什么是 hook 函数

  • 比如说你写了一个框架类的程序,你希望这个框架可以“被其他的代码注入”,即别人可以加入代码对你这个框架进行定制化,该如何做比较好?
  • 一种很常见的方式就是约定一个规则,框架初始化时会收集满足这个规则的所有代码(文件),然后把这些代码加入到框架中来,在执行时一并执行。所有这一规则下可以被框架收集到的方法就是hook方法

pytest 加载插件的方式

  • 内置plugins:从代码内部的_pytest目录加载

  • 外部插件(第三方插件):通过setuptools entry points机制发现的第三方插件模块;

    推荐的第三方的pytest的插件:https://docs.pytest.org/en/latest/plugins.html

  • conftest.py形式的本地插件:测试目录下的自动模块发现机制

    通过pytest --trace-config命令可以查看当前pytest中所有的plugin。

编写自己的插件

命令行添加自定义参数

# conftest.py 文件
# 注册一个命令行参数--cmdopt
def pytest_addoption(parser):
    parser.addoption(
        "--cmdopt",
        action="store",
        default="devices_info",
        help="What is needed to start the session udid and port,type is dict"
    )

# 创建一个 fixture 获取命令行参数--cmdopt 的值
@pytest.fixture()  # session 作用域的话,每次执行用例不会重启 app
def cmdopt(request):
    return request.config.getoption("--cmdopt")
# test_simple.py 文件
class TestDemo(object):

    def test_004(self, cmdopt):
        print("获取到的设备名称:{}".format(cmdopt))
        assert 4 == 4

    def test_005(self, cmdopt):
        print("获取到的设备名称:{}".format(cmdopt))
        assert 4 == 5


pytest.main(['test_simple.py::TestDemo',
             '-vs', '--cmdopt=iphone'])

collecting ... ============================= test session starts ==============================
platform darwin -- Python 3.7.8, pytest-6.2.1, py-1.9.0, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Darwin-19.6.0-x86_64-i386-64bit', 'Packages': {'pytest': '6.2.1', 'py': '1.9.0', 'pluggy': '0.13.1'}, 'Plugins': {'assume': '2.4.2', 'rerunfailures': '9.1.1', 'html': '3.1.1', 'allure-pytest': '2.8.16', 'metadata': '1.11.0', 'dependency': '0.5.1', 'ordering': '0.6', 'repeat': '0.9.1', 'xdist': '2.2.0', 'forked': '1.3.0'}}
rootdir: /Users/mac/Desktop/download/UIAutoTest/DemoPytest
plugins: assume-2.4.2, rerunfailures-9.1.1, html-3.1.1, allure-pytest-2.8.16, metadata-1.11.0, dependency-0.5.1, ordering-0.6, repeat-0.9.1, xdist-2.2.0, forked-1.3.0
collecting ... collected 2 items

test_simple.py::TestDemo::test_004 获取到的设备名称:iphone
PASSED
test_simple.py::TestDemo::test_005 获取到的设备名称:iphone
FAILED

=================================== FAILURES ===================================
______________________________ TestDemo.test_005 _______________________________

self = <DemoPytest.test_simple.TestDemo object at 0x10576e0d0>
cmdopt = 'iphone'

    def test_005(self, cmdopt):
        print("获取到的设备名称:{}".format(cmdopt))
>       assert 4 == 5
E       assert 4 == 5
E         +4
E         -5

test_simple.py:46: AssertionError
=========================== short test summary info ============================
FAILED test_simple.py::TestDemo::test_005 - assert 4 == 5
========================= 1 failed, 1 passed in 0.10s ==========================
collected 2 items

test_simple.py::TestDemo::test_004 获取到的设备名称:iphone
PASSED
test_simple.py::TestDemo::test_005 获取到的设备名称:iphone
FAILED

  • parser.addoption()参数说明

    name:自定义命令行参数的名字,可以是:“foo”, “-foo” 或 “–foo”;
    action:在命令行中遇到此参数时要采取的基本操作类型;
    nargs:应该使用的命令行参数的数量;
    const:某些操作和nargs选择所需的常量值;
    default:如果参数不在命令行中,则生成的默认值。
    type:命令行参数应该转换为的类型;
    choices:参数允许值的容器;
    required:命令行选项是否可以省略(仅可选);
    help:对参数作用的简要说明;
    metavar:用法消息中参数的名称;
    dest:要添加到 parse_args() 返回的对象中的属性的名称;
    
  • 使用场景

    • 使用 UI 自动化时,可以指定手机设备,或者不同的浏览器
    • 接口自动化时,可以指定正式环境,测试环境分别读取不同的配置文件数据

获取每个用例详细的执行结果

  • pytest_runtest_markreport()
  • 可以获取到测试用例的详细执行结果,setup 是否执行成功,call 测试用例是否执行成功,teardown 是否执行成功
# conftest.py 文件
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    print('*'*50)
    # 获取钩子方法的调用结果
    out = yield
    print('用例执行结果', out)
    # 从钩子方法的调用结果中获取测试报告
    report = out.get_result()
    print('测试报告:{}'.format(report))
    print('步骤:{}'.format(report.when))
    print('nodeid:{}'.format(report.nodeid))
    print('description:{}'.format(str(item.function.__doc__)))
    print(('运行结果: {}'.format(report.outcome)))
    print('*'*50)
    
# 与上面一样,只是通过 when 进行了阶段的区分    
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
    print('*' * 50)
    # 获取钩子方法的调用结果
    out = yield
    print('用例执行结果', out)
    # 从钩子方法的调用结果中获取测试报告
    report = out.get_result()
    # 通过 when 来区分 setup,call,teardown 这三个阶段
    if report.when == "call":
        print('测试报告:{}'.format(report))
        print('步骤:{}'.format(report.when))
        print('nodeid:{}'.format(report.nodeid))
        print('description:{}'.format(str(item.function.__doc__)))
        print(('运行结果: {}'.format(report.outcome)))
        print('*' * 50)
# test_simple.py 文件
class TestDemo(object):

    def test_004(self, cmdopt):
        assert 4 == 4

    def test_005(self, cmdopt):
        assert 4 == 5


pytest.main(['test_simple.py::TestDemo',
             '-v'])
/usr/local/bin/python3.7 /Users/mac/Desktop/download/UIAutoTest/DemoPytest/test_simple.py
============================= test session starts ==============================
platform darwin -- Python 3.7.8, pytest-6.2.1, py-1.9.0, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Darwin-19.6.0-x86_64-i386-64bit', 'Packages': {'pytest': '6.2.1', 'py': '1.9.0', 'pluggy': '0.13.1'}, 'Plugins': {'assume': '2.4.2', 'rerunfailures': '9.1.1', 'html': '3.1.1', 'allure-pytest': '2.8.16', 'metadata': '1.11.0', 'dependency': '0.5.1', 'ordering': '0.6', 'repeat': '0.9.1', 'xdist': '2.2.0', 'forked': '1.3.0'}}
rootdir: /Users/mac/Desktop/download/UIAutoTest/DemoPytest
plugins: assume-2.4.2, rerunfailures-9.1.1, html-3.1.1, allure-pytest-2.8.16, metadata-1.11.0, dependency-0.5.1, ordering-0.6, repeat-0.9.1, xdist-2.2.0, forked-1.3.0
collecting ... collected 2 items

test_simple.py::TestDemo::test_004 **************************************************
用例执行结果 <pluggy.callers._Result object at 0x1053d5650>
测试报告:<TestReport 'test_simple.py::TestDemo::test_004' when='setup' outcome='passed'>
步骤:setup
nodeid:test_simple.py::TestDemo::test_004
description:None
运行结果: passed
**************************************************
**************************************************
用例执行结果 <pluggy.callers._Result object at 0x105413050>
测试报告:<TestReport 'test_simple.py::TestDemo::test_004' when='call' outcome='passed'>
步骤:call
nodeid:test_simple.py::TestDemo::test_004
description:None
运行结果: passed
**************************************************
PASSED                                [ 50%]**************************************************
用例执行结果 <pluggy.callers._Result object at 0x105413050>
测试报告:<TestReport 'test_simple.py::TestDemo::test_004' when='teardown' outcome='passed'>
步骤:teardown
nodeid:test_simple.py::TestDemo::test_004
description:None
运行结果: passed
**************************************************

test_simple.py::TestDemo::test_005 **************************************************
用例执行结果 <pluggy.callers._Result object at 0x1053bba10>
测试报告:<TestReport 'test_simple.py::TestDemo::test_005' when='setup' outcome='passed'>
步骤:setup
nodeid:test_simple.py::TestDemo::test_005
description:None
运行结果: passed
**************************************************
**************************************************
用例执行结果 <pluggy.callers._Result object at 0x105454710>
测试报告:<TestReport 'test_simple.py::TestDemo::test_005' when='call' outcome='failed'>
步骤:call
nodeid:test_simple.py::TestDemo::test_005
description:None
运行结果: failed
**************************************************
FAILED                                [100%]**************************************************
用例执行结果 <pluggy.callers._Result object at 0x105438d10>
测试报告:<TestReport 'test_simple.py::TestDemo::test_005' when='teardown' outcome='passed'>
步骤:teardown
nodeid:test_simple.py::TestDemo::test_005
description:None
运行结果: passed
**************************************************


=================================== FAILURES ===================================
______________________________ TestDemo.test_005 _______________________________

self = <DemoPytest.test_simple.TestDemo object at 0x105426790>

    def test_005(self):
>       assert 4 == 5
E       assert 4 == 5
E         +4
E         -5

test_simple.py:44: AssertionError
=========================== short test summary info ============================
FAILED test_simple.py::TestDemo::test_005 - assert 4 == 5
========================= 1 failed, 1 passed in 0.23s ==========================

Process finished with exit code 0

改变用例执行顺序

  • pytest_collection_modifyitems(session, items)

  • 可以通过此 hook 函数改变收集到用例的执行顺序

# conftest.py 文件
def pytest_collection_modifyitems(session, items):
    print("收集到的测试用例:%s" % items)
    random.shuffle(items) # 将收集到的用例进行随机
    print("随机排序后的用例:%s" % items)
# test_simple.py 文件
class TestDemo(object):
    def test_004(self):
        assert 4 == 4

    def test_b_002(self):
        assert 1 == 1

    def test_a_002(self):
        assert 1 == 1

    def test_005(self):
        assert 4 == 5

    def test_b_001(self):
        assert 1 == 1

    def test_a_001(self):
        assert 1 == 1


pytest.main(['test_simple.py::TestDemo',
             '-v'])
/usr/local/bin/python3.7 /Users/mac/Desktop/download/UIAutoTest/DemoPytest/test_simple.py
============================= test session starts ==============================
platform darwin -- Python 3.7.8, pytest-6.2.1, py-1.9.0, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Darwin-19.6.0-x86_64-i386-64bit', 'Packages': {'pytest': '6.2.1', 'py': '1.9.0', 'pluggy': '0.13.1'}, 'Plugins': {'assume': '2.4.2', 'rerunfailures': '9.1.1', 'html': '3.1.1', 'allure-pytest': '2.8.16', 'metadata': '1.11.0', 'dependency': '0.5.1', 'ordering': '0.6', 'repeat': '0.9.1', 'xdist': '2.2.0', 'forked': '1.3.0'}}
rootdir: /Users/mac/Desktop/download/UIAutoTest/DemoPytest
plugins: assume-2.4.2, rerunfailures-9.1.1, html-3.1.1, allure-pytest-2.8.16, metadata-1.11.0, dependency-0.5.1, ordering-0.6, repeat-0.9.1, xdist-2.2.0, forked-1.3.0
collecting ... 收集到的测试用例:[<Function test_004>, <Function test_b_002>, <Function test_a_002>, <Function test_005>, <Function test_b_001>, <Function test_a_001>]
随机排序后的用例:[<Function test_b_001>, <Function test_004>, <Function test_b_002>, <Function test_005>, <Function test_a_001>, <Function test_a_002>]
collected 6 items

test_simple.py::TestDemo::test_b_001 PASSED                              [ 16%]
test_simple.py::TestDemo::test_004 PASSED                                [ 33%]
test_simple.py::TestDemo::test_b_002 PASSED                              [ 50%]
test_simple.py::TestDemo::test_005 FAILED                                [ 66%]
test_simple.py::TestDemo::test_a_001 PASSED                              [ 83%]
test_simple.py::TestDemo::test_a_002 PASSED                              [100%]

=================================== FAILURES ===================================
______________________________ TestDemo.test_005 _______________________________

self = <DemoPytest.test_simple.TestDemo object at 0x1110799d0>

    def test_005(self):
>       assert 4 == 5
E       assert 4 == 5
E         +4
E         -5

test_simple.py:50: AssertionError
=========================== short test summary info ============================
FAILED test_simple.py::TestDemo::test_005 - assert 4 == 5
========================= 1 failed, 5 passed in 0.26s ==========================

Process finished with exit code 0

统计测试结果

  • pytest_terminal_summary(terminalreporter, exitstatus, config)
  • 通过此 hook 函数可以获取到用例执行的结果,通过数,失败数,跳过数,总数,执行时间等
# conftest.py
# 收集测试结果
def pytest_terminal_summary(terminalreporter, exitstatus, config):
    print("收集测试结果开始:")
    print("*"*30)
    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, 's')
    print("收集测试结果结束:")
    print("*" * 30)
# test_simple.py 文件
class TestDemo(object):
    def test_004(self):
        assert 4 == 4

    def test_b_002(self):
        assert 1 == 1

    def test_a_002(self):
        assert 1 == 1

    def test_005(self):
        assert 4 == 5

    def test_b_001(self):
        assert 1 == 1

    def test_a_001(self):
        assert 1 == 1


pytest.main(['test_simple.py::TestDemo',
             '-v'])
/usr/local/bin/python3.7 /Users/mac/Desktop/download/UIAutoTest/DemoPytest/test_simple.py
============================= test session starts ==============================
platform darwin -- Python 3.7.8, pytest-6.2.1, py-1.9.0, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Darwin-19.6.0-x86_64-i386-64bit', 'Packages': {'pytest': '6.2.1', 'py': '1.9.0', 'pluggy': '0.13.1'}, 'Plugins': {'assume': '2.4.2', 'rerunfailures': '9.1.1', 'html': '3.1.1', 'allure-pytest': '2.8.16', 'metadata': '1.11.0', 'dependency': '0.5.1', 'ordering': '0.6', 'repeat': '0.9.1', 'xdist': '2.2.0', 'forked': '1.3.0'}}
rootdir: /Users/mac/Desktop/download/UIAutoTest/DemoPytest
plugins: assume-2.4.2, rerunfailures-9.1.1, html-3.1.1, allure-pytest-2.8.16, metadata-1.11.0, dependency-0.5.1, ordering-0.6, repeat-0.9.1, xdist-2.2.0, forked-1.3.0
collecting ... collected 6 items

test_simple.py::TestDemo::test_004 PASSED                                [ 16%]
test_simple.py::TestDemo::test_b_002 PASSED                              [ 33%]
test_simple.py::TestDemo::test_a_002 PASSED                              [ 50%]
test_simple.py::TestDemo::test_005 FAILED                                [ 66%]
test_simple.py::TestDemo::test_b_001 PASSED                              [ 83%]
test_simple.py::TestDemo::test_a_001 PASSED                              [100%]

=================================== FAILURES ===================================
______________________________ TestDemo.test_005 _______________________________

self = <DemoPytest.test_simple.TestDemo object at 0x110dbfe50>

    def test_005(self):
>       assert 4 == 5
E       assert 4 == 5
E         +4
E         -5

test_simple.py:50: AssertionError
收集测试结果开始:
******************************
{'': [<TestReport 'test_simple.py::TestDemo::test_004' when='setup' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_004' when='teardown' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_b_002' when='setup' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_b_002' when='teardown' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_a_002' when='setup' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_a_002' when='teardown' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_005' when='setup' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_005' when='teardown' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_b_001' when='setup' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_b_001' when='teardown' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_a_001' when='setup' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_a_001' when='teardown' outcome='passed'>], 'passed': [<TestReport 'test_simple.py::TestDemo::test_004' when='call' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_b_002' when='call' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_a_002' when='call' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_b_001' when='call' outcome='passed'>, <TestReport 'test_simple.py::TestDemo::test_a_001' when='call' outcome='passed'>], 'failed': [<TestReport 'test_simple.py::TestDemo::test_005' when='call' outcome='failed'>]}
total: 6
passed: 5
failed: 1
error: 0
skipped: 0
total times: 0.48371005058288574 seconds
收集测试结果结束:
******************************
=========================== short test summary info ============================
FAILED test_simple.py::TestDemo::test_005 - assert 4 == 5
========================= 1 failed, 5 passed in 0.48s ==========================

Process finished with exit code 0

pytest 的运行原理

推荐阅读