Pytest 测试框架

Pytest 测试框架

一. 快速安装pytest环境

  1. 选择合适的系统版本, 安装python运行环境

  2. 安装pytest

    1
    pip install pytest
  3. 安装pytest常用插件

    1
    2
    3
    4
    5
    6
    7
    pip install pytest-html
    # 多线程或分布式运行测试用例插件
    pip install pytest-xdist
    # 用例失败后重跑插件
    pip install pytest-rerunfailures
    # 设置用例超时时间
    pip install pytest-timeout
  4. 安装完成后使用pip list 指令pip list | findstr "pytest"查看安装结果,如下:

二. 如何使用

pytest 常见配置文件

  • pytest.ini :pytest的主配置文件,可以改变pytest的默认行为,有很多可配置的选项。

  • conftest.py :是本地的插件库,其中的hook函数和fixture将作用于该文件所在的目录以及所有子目录。

  • init.py :每个测试子目录都包含该文件时,那么在多个测试目录中可以出现同名测试文件。

  • tox.ini :它与pytest.ini类似,只不过是tox的配置文件,你可以把pytest的配置都写在tox.ini里,这样就不用同时使用tox.ini和pytest.ini两个文件

pytest_construct.png

三. 测试类

将多个测试函数组织在一个类中,类名以 Test 开头(否则 pytest 不会自动发现)

适用于逻辑相关的测试, 比如一个类型的测试任务可以放在一个类中,可以使用setup_methodteardown_method进行初始化和清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#### test_server.py 测试模块
import logging
def add(a, b):
return a + b
#### 测试类, 类名以Test开头,定义在模块中
class TestServer:
def setup_method(self, method):
logging.info("This is setup method %s" %(method.__name__))
def teardown_method(self, method):
logging.info("This is teardown method %s"%({method.__name__}))
def test_add_int(self,):
assert add(1, 2) == 3
def test_add_str(self):
assert add("1", "2") == "3"

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
..\testCase\test_demo01.py::test_case_01 PASSED  [ 33%]
..\testCase\test_server.py::TestServer::test_add_int
---------------------------------------- live log setup ----------------------------------
2025-05-02 16:22:17 [INFO] This is setup method test_add_int PASSED [ 66%]
---------------------------------------- live log teardown --------------------------------
2025-05-02 16:22:17 [INFO] This is teardown method {'test_add_int'}
..\testCase\test_server.py::TestServer::test_add_str
---------------------------------------- live log setup ----------------------------------
2025-05-02 16:22:17 [INFO] This is setup method test_add_str FAILED [100%]
---------------------------------------- live log teardown -------------------------------
2025-05-02 16:22:17 [INFO] This is teardown method {'test_add_str'}
======================================== FAILURES
________________________________________ TestServer.test_add_str
self = <testCase.test_server.TestServer object at 0x000001975F56A090>
def test_add_str(self):
> assert add("1", "2") == "3"
E AssertionError: assert '12' == '3'
E
E - 3
E + 12
..\testCase\test_server.py:16: AssertionError
---------------------------------------- Captured log setup --------------------------------
2025-05-02 16:22:17 [INFO] This is setup method test_add_str
---------------------------------------- Captured log teardown ------------------------------
2025-05-02 16:22:17 [INFO] This is teardown method {'test_add_str'}

setup_methodteardown_method会对测试类里面的每一个测试方法都执行

四. 测试模块

一个test_*.py文件就是一个测试模块, 一个测试模块可以包含多个测试函数或测试类, 适用于组织同一模块的多个测试. setup_moduleteardown_module进行模块级的初始化和清理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#### test_server.py 测试模块
import logging
def setup_module(module):
logging.info(f"This is Setup module {module.__name__}")
def teardown_module(module):
logging.info(f"This is Teardown module {module.__name__}")
def add(a, b):
return a + b
#### 测试函数, 以test_开头, 直接定义在模块中
def test_add_list():
assert add([1], [2,3,4]) == [1, 2, 3, 4]
#### 测试类, 类名以Test开头, 定义在模块中
class TestServer:
def setup_method(self, method):
logging.info("This is class setup method %s" %(method.__name__))
def teardown_method(self, method):
logging.info("This is class teardown method %s"%({method.__name__}))
def test_add_int(self,):
assert add(1, 2) == 3
def test_add_str(self):
assert add("1", "2") == "12"

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..\testCase\test_demo01.py::test_case_01 PASSED  [ 25%] 
..\testCase\test_server.py::test_add_list
--------------------------------- live log setup -------------------------------------
2025-05-02 17:19:13 [INFO] This is Setup module testCase.test_server PASSED [ 50%]
..\testCase\test_server.py::TestServer::test_add_int
--------------------------------- live log setup -------------------------------------
2025-05-02 17:19:13 [INFO] This is class setup method test_add_int PASSED [ 75%]
--------------------------------- live log teardown ---------------------------------
2025-05-02 17:19:13 [INFO] This is class teardown method {'test_add_int'}
..\testCase\test_server.py::TestServer::test_add_str
--------------------------------- live log setup ------------------------------------
2025-05-02 17:19:13 [INFO] This is class setup method test_add_str PASSED [100%]
--------------------------------- live log teardown ----------------------------------
2025-05-02 17:19:13 [INFO] This is class teardown method {'test_add_str'}
2025-05-02 17:19:13 [INFO] This is Teardown module testCase.test_server

从测试结果可以看到,会在运行本测试模块前先运行一次 setup_module, 然后会对测试类中的每个测试方法运行前执行setup_method , 运行结束后执行teardown_method最后本文件的测试模块都运行完成后执行teardown_module.

五. 夹具fixture

创建fixture

  1. 创建函数

  2. 添加装饰器

  3. 添加yield关键字

    1
    2
    3
    4
    5
    @pytest.fixture
    def fix():
    logging.info("This is fixture before")
    yield
    logging.info("This is fixture after")

使用fixture

  1. 给用例的参数列表中,加入fixture名字即可

    1
    2
    def test_add_list(fix):
    assert add([1], [2,3,4]) == [1, 2, 3, 4]

    如果在测试类中的方法既添加了fixture, 也添加了setup_methodteardown_method, 那它们执行的先后顺序是 setup_method -> fixture yield before -> fixture yield after -> teardown_method.

  2. 用例加上usefixtures标记

    1
    2
    3
    @pytest.mark.usefixtures("fix")
    def test_add_list():
    assert add([1], [2,3,4]) == [1, 2, 3, 4]

高级用法

1. 依赖使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@pytest.fixture
def ff():
logging.info("This is secend setup function")
yield
logging.info("This is secend teardown function")
@pytest.fixture
def fix(ff):
logging.info("This is first fixture before")
yield
logging.info("This is first fixture after")
def add(a, b):
return a + b
class TestServer:
def setup_method(self, method):
logging.info("This is class setup method %s" %(method.__name__))
def teardown_method(self, method):
logging.info("This is class teardown method %s"%({method.__name__}))
def test_add_int(self,fix):
assert add(1, 2) == 3

运行结果:

1
2
3
4
5
6
7
8
9
..\testCase\test_server.py::TestServer::test_add_int
------------------------------------- live log setup ----------------------------------------
2025-05-02 19:28:33 [INFO] This is class setup method test_add_int
2025-05-02 19:28:33 [INFO] This is secend setup function
2025-05-02 19:28:33 [INFO] This is first fixture before PASSED [ 75%]
------------------------------------- live log teardown ------------------------------------
2025-05-02 19:28:33 [INFO] This is first fixture after
2025-05-02 19:28:33 [INFO] This is secend teardown function
2025-05-02 19:28:33 [INFO] This is class teardown method {'test_add_int'}

2. 自动使用

1
2
3
4
5
6
7
8
9
10
@pytest.fixture
def ff():
logging.info("This is secend setup function")
yield
logging.info("This is secend teardown function")
@pytest.fixture(autouse=True)
def fix(ff):
logging.info("This is first fixture before")
yield
logging.info("This is first fixture after")

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
..\testCase\test_demo01.py::test_case_01 PASSED  [ 25%]
..\testCase\test_server.py::test_add_list
------------------------------- live log setup --------------------------------------
2025-05-02 19:34:49 [INFO] This is Setup module testCase.test_server
2025-05-02 19:34:49 [INFO] This is secend setup function
2025-05-02 19:34:49 [INFO] This is first fixture before PASSED [ 50%]
------------------------------- live log teardown -----------------------------------
2025-05-02 19:34:49 [INFO] This is first fixture after
2025-05-02 19:34:49 [INFO] This is secend teardown function
..\testCase\test_server.py::TestServer::test_add_int
------------------------------- live log setup --------------------------------------
2025-05-02 19:34:49 [INFO] This is secend setup function
2025-05-02 19:34:49 [INFO] This is first fixture before
2025-05-02 19:34:49 [INFO] This is class setup method test_add_int PASSED [ 75%]
------------------------------- live log teardown -----------------------------------
2025-05-02 19:34:49 [INFO] This is class teardown method {'test_add_int'}
2025-05-02 19:34:49 [INFO] This is first fixture after
2025-05-02 19:34:49 [INFO] This is secend teardown function
..\testCase\test_server.py::TestServer::test_add_str
------------------------------- live log setup --------------------------------------
2025-05-02 19:34:49 [INFO] This is secend setup function
2025-05-02 19:34:49 [INFO] This is first fixture before
2025-05-02 19:34:49 [INFO] This is class setup method test_add_str PASSED [100%]
------------------------------- live log teardown -----------------------------------
2025-05-02 19:34:49 [INFO] This is class teardown method {'test_add_str'}
2025-05-02 19:34:49 [INFO] This is first fixture after
2025-05-02 19:34:49 [INFO] This is secend teardown function
2025-05-02 19:34:49 [INFO] This is Teardown module testCase.test_server

如果在测试类中的方法既添加了fixture, 也添加了setup_methodteardown_method, 并且autouse设为True,注意类里面的函数执行先手顺序变了, secend fixture yield before -> first fixture yield before -> setup_method -> teardown_method -> first fixture yield after -> secend fixture yield after .

3. 返回内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@pytest.fixture
def ff():
logging.info("This is secend setup function")
obtain_user = "joharynd"
yield obtain_user
logging.info("This is secend teardown function")
@pytest.fixture
def fix(ff):
logging.info("This is first fixture before")
obtain_pwd = "123456"
info = {"username": ff, "password": obtain_pwd}
yield info
logging.info("This is first fixture after")
def test_add_str(self,fix):
logging.info(f"The password is {fix}")
assert add("1", "2") == "12"

运行结果:

1
2
3
4
5
6
7
8
--------------------------------- live log setup ---------------------------------------- 
2025-05-02 20:05:31 [INFO] This is secend setup function
2025-05-02 20:05:31 [INFO] This is first fixture before
--------------------------------- live log call -----------------------------------------
2025-05-02 20:05:31 [INFO] The password is {'username': 'joharynd', 'password': '123456'} PASSED [100%]
--------------------------------- live log teardown -------------------------------------
2025-05-02 20:05:31 [INFO] This is first fixture after
2025-05-02 20:05:31 [INFO] This is secend teardown function

4. 范围共享

  • 默认范围: function

  • 全局范围:session

    Scope 作用范围 执行次数
    function 每个测试函数 每个测试运行一次
    class 每个测试类 每个类运行一次
    module 每个测试模块 每个模块运行一次
    package 每个测试包 每个包运行一次
    session 整个测试会话 只运行一次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import logging
def setup_module(module):
logging.info(f"This is Setup module {module.__name__}")
def teardown_module(module):
logging.info(f"This is Teardown module {module.__name__}")
@pytest.fixture(scope="module")
def ff():
logging.info("This is secend fixture function")
obtain_user = "joharynd"
yield obtain_user
logging.info("This is secend fixture function")
@pytest.fixture(scope="module",autouse=True)
def fix(ff):
logging.info("This is first fixture before")
obtain_pwd = "123456"
info = {"username": ff, "password": obtain_pwd}
yield info
logging.info("This is first fixture after")
def add(a, b):
return a + b
def test_add_list():
assert add([1], [2,3,4]) == [1, 2, 3, 4]
class TestServer:
def setup_method(self, method):
logging.info("This is class setup method %s" %(method.__name__))
def teardown_method(self, method):
logging.info("This is class teardown method %s"%({method.__name__}))
def test_add_int(self):
assert add(1, 2) == 3
def test_add_str(self,fix):
logging.info(f"The password is {fix}")
assert add("1", "2") == "12"

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
..\testCase\test_demo01.py::test_case_01 PASSED [ 25%] 
..\testCase\test_server.py::test_add_list
------------------------- live log setup ---------------------------------
2025-05-02 20:19:29 [INFO] This is Setup module testCase.test_server
2025-05-02 20:19:29 [INFO] This is secend fixture function
2025-05-02 20:19:29 [INFO] This is first fixture before PASSED [ 50%]
..\testCase\test_server.py::TestServer::test_add_int
------------------------ live log setup ----------------------------------
2025-05-02 20:19:29 [INFO] This is class setup method test_add_int PASSED [ 75%]
------------------------ live log teardown -------------------------------
2025-05-02 20:19:29 [INFO] This is class teardown method {'test_add_int'}
..\testCase\test_server.py::TestServer::test_add_str
------------------------ live log setup ----------------------------------
2025-05-02 20:19:29 [INFO] This is class setup method test_add_str
------------------------ live log call -----------------------------------
2025-05-02 20:19:29 [INFO] The password is {'username': 'joharynd', 'password': '123456'} PASSED [100%]
------------------------ live log teardown -------------------------------
2025-05-02 20:19:29 [INFO] This is class teardown method {'test_add_str'}
2025-05-02 20:19:29 [INFO] This is first fixture after
2025-05-02 20:19:29 [INFO] This is secend fixture function
2025-05-02 20:19:29 [INFO] This is Teardown module testCase.test_server

在测试类中的方法中引用了fixture fix, 同时将fixture fix和ff的生效范围改成module, 可以看到fix和ff值生效了一次, 并且类里面的方法引用了fix, 也只是获取到yield传递的参数.

  • 使用conftest.py 进行整个session的共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import logging
import pytest
@pytest.fixture(scope="session")
def ff():
logging.info("This is secend fixture function")
obtain_user = "joharynd"
yield obtain_user
logging.info("This is secend fixture function")
@pytest.fixture(scope="session",autouse=True)
def fix(ff):
logging.info("This is first fixture before")
obtain_pwd = "123456"
info = {"username": ff, "password": obtain_pwd}
yield info
logging.info("This is first fixture after")

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
..\testCase\test_demo01.py::test_case_01
------------------------- live log setup -----------------------------------
2025-05-02 20:57:20 [INFO] This is secend fixture function
2025-05-02 20:57:20 [INFO] This is first fixture before PASSED [ 25%]
..\testCase\test_server.py::test_add_list
------------------------- live log setup -----------------------------------
2025-05-02 20:57:20 [INFO] This is Setup module testCase.test_server PASSED [ 50%]
..\testCase\test_server.py::TestServer::test_add_int
------------------------- live log setup -----------------------------------
2025-05-02 20:57:20 [INFO] This is class setup method test_add_int PASSED [ 75%]
------------------------- live log teardown --------------------------------
2025-05-02 20:57:20 [INFO] This is class teardown method {'test_add_int'}
..\testCase\test_server.py::TestServer::test_add_str
------------------------- live log setup -----------------------------------
2025-05-02 20:57:20 [INFO] This is class setup method test_add_str
------------------------- live log call ------------------------------------
2025-05-02 20:57:20 [INFO] The password is {'username': 'joharynd', 'password': '123456'} PASSED [100%]
------------------------- live log teardown --------------------------------
2025-05-02 20:57:20 [INFO] This is class teardown method {'test_add_str'}
2025-05-02 20:57:20 [INFO] This is Teardown module testCase.test_server
2025-05-02 20:57:20 [INFO] This is first fixture after
2025-05-02 20:57:20 [INFO] This is secend fixture function

六. 插件管理

插件分为两类:

  • 不需要安装的插件: 内置插件
  • 需要安装的插件: 第三方插件

插件的启用管理:

  • 启用: -p plugxxx
  • 禁用: -p no:plugxxx

插件的使用方式:

  • 命令行参数: pytest testxxx.py –html=./result/result.html
  • 配置文件: pytest.iniaddopts
  • fixture
  • mark

常用的第三方插件

1. pytest-html插件

用于生成HTML测试报告

安装:

1
pip install pytest-html

用法:

1
pytest --html=./results/result.html --self-contained-html

2. pytest-xdist 插件

用于分布式执行测试用例

只有在任务本身耗时较长,超出调用成本很多的时候,才有意义

分布式执行,有可能会出现并发问题: 资源竞争、乱序

安装:

1
pip install pytest-xdist

使用:

1
pytest -n 10 //用10个进程来执行测试用例

3. pytest-rerunfailures插件

用例失败后重新执行

安装:

1
pip install pytest-rerunfailures

使用:

  1. 使用 @pytest.mark.flaky 装饰器

    为单个测试用例使用 @pytest.mark.flaky 装饰器设置重跑次数

    1
    2
    3
    4
    5
    import pytest
    import random
    @pytest.mark.flaky(reruns=3, reruns_delay=2) # 设置重跑次数为3次,每次重跑间隔2s
    def test_unstable_function():
    assert random.choice([True, False])
  2. 通过配置文件为每个测试用例设置重跑次数

    通过在 pytest.ini 配置默认重跑次数为每个用例设置失败重跑次数

    1
    2
    3
    4
    5
    [pytest]
    reruns = 3 ; 为所有用例设置失败重跑次数为3次
    rerun_delay = 2 ; 每次失败重跑的时间间隔为2s
    [pytest:test_unstable_function]
    reruns = 5 ; 也可以为特定用例设置不同的重跑次数
  3. 通过命令行参数为所有用例设置重跑参数

    1
    pytest --reruns 3 --reruns-delay 2 filename

4. pytest-result-log

把用例的执行结果记录到日志文件中

安装:

1
pip install pytest-result-log

使用:

pytest.ini文件中进行配置

1
2
3
4
5
6
7
8
9
10
11
12
log_file = ./results/result.log
log_file_level = info
log_file_format = %(levelname)-8s %(asctime)s [%(name)s:%(lineno)s] : %(message)s
log_file_date_format = %Y-%m-%d %H:%M:%S
;记录用例执行结果
result_log_enable = 1
;记录用例分割线
result_log_separator = 1
;分割线等级
result_log_level_separator = warning
;异常信息等级
result_log_level_verbose = info

七. 企业级测试报告

allure是一个测试报告框架

1
2
3
4
5
6
pip install allure-pytest

配置使用:
--alluredir=temps --clean-alluredir
生成报告:
allure generate -o report(生成报告路径) -c temps(加载元数据路径)

allure支持对用例进行分组和关联(敏捷开发的术语)

1
2
3
4
5
6
import allure

@allure.epic //史诗,可用来表示一个项目,最大的一个分组
@allure.feature //主题, 可用来表示一个模块
@allure.story //故事, 可用来表示一个功能
@allure.title //标题, 可以用来说明一个用例

八. 扩展

将日志打印信息添加到输出的网页结果里面

  1. 修改pytest.ini配置,确保启用日志捕获,并配置日志级别和输出格式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [pytest]
    addopts = -vs -m "cycle or performance" --html=./results/result.html --self-contained-html
    # 启用命令行日志输出
    log_cli = true
    # 捕获 INFO 及以上级别的日志
    log_cli_level = INFO
    # 日志格式
    log_format = %(asctime)s [%(levelname)s] %(message)s
    # 时间格式
    log_date_format = %Y-%m-%d %H:%M:%S
    # timeout = 10 ;所有用例的超时时间为10s钟
  2. 在测试代码中通过 pytest 钩子添加日志到报告,在 conftest.py 文件中添加以下代码,将日志信息嵌入 HTML 报告:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import logging
    def pytest_runtest_makereport(item, call):
    # 获取测试阶段的日志内容
    if call.when == 'call':
    logs = []
    for handler in logging.getLogger().handlers:
    if hasattr(handler, 'captured_logs'):
    logs.extend(handler.captured_logs)

    # 将日志添加到 HTML 报告的额外信息中
    if logs:
    report = item._report
    report.sections.append(("Logs", "\n".join(logs)))
  3. 修改测试代码,确保日志被捕获

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import pytest
    import logging
    from timeout_decorator import timeout
    @pytest.mark.parametrize("a,b,c",[(1,2,3),(2,3,6),(3,4,7)])
    @pytest.mark.cycle
    //@pytest.mark.timeout(180) #设置超时时间为3min
    @timeout(5)
    def test_add(a,b,c):
    # caplog.set_level(logging.INFO)
    logging.info(f"{a}+{b}={c}")
    assert a+b == c, f"Expected {a} + {b} to be {c}, but got {a + b}"