Akemi

Python-pytest框架

2025/02/21

本文主要是一些理论知识,以及一些案例,函数之间的对比

适合像我一样没有测试基础的人看,我就是先学一手

pytest是Python的一个第三方单元测试框架,相比Python自带的unittest框架,它更加简洁和高效,同时兼容unittest框架。

测试实践

环境 用途 测试活动
开发环境 开发者编写和调试代码 本地运行单元测试、手动测试
测试环境 模拟生产环境的独立环境 运行自动化测试(单元测试、集成测试、端到端测试)
预生产环境 与生产环境几乎一致的镜像环境 性能测试、安全扫描、最终验收测试
生产环境 用户实际使用的线上环境 不运行测试代码,仅通过监控、日志、告警确保应用健康

单元测试(验证函数逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
# src/math_utils.py
def divide(a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b

# tests/test_math_utils.py
def test_divide():
assert divide(4, 2) == 2.0

def test_divide_by_zero():
with pytest.raises(ValueError):
divide(4, 0)

集成测试(验证多模块协作)

1
2
3
4
5
6
# tests/test_api.py
def test_user_login(client):
# 测试 API 登录功能(client 是模拟的 Flask 客户端)
response = client.post("/login", json={"username": "admin", "password": "secret"})
assert response.status_code == 200
assert "access_token" in response.json()

端到端测试(模拟用户操作)

1
2
3
4
5
6
7
# tests/e2e/test_checkout.py
def test_checkout_flow(browser):
# 使用 Selenium 模拟浏览器操作
browser.get("<https://example.com>")
browser.find_element(By.ID, "add-to-cart").click()
browser.find_element(By.ID, "checkout").click()
assert "订单完成" in browser.title

运行pytest的方式

使用pytest命令行运行

也就是pip install pytest后,直接使用pytest命令就可以运行

python项目目录简化后,基本是这么一个结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
C:.
└─test_project
├─src
└─tests

pytest命令如果直接使用
会自动递归搜索符合以下命名条件的文件
1.以 test_ 开头的文件(例如 test_example.py)。
2.以 _test 结尾的文件(例如 example_test.py)。
3.文件名中包含test的文件(例如 mytestfile.py)

单个测试时,可用pytest直接指定测试文件,或其中的某个函数
如pytest example_test.py::test_addition

其他命令行可用参数

  • -s:显示测试执行过程中的标准输出和错误输出,包括print打印的信息。
  • -v:显示更详细的测试执行信息,包括测试用例的名称、执行状态和执行时间等。
  • -q:显示简略的测试执行信息。
  • -k:只执行包含指定关键字的测试用例。关键字可以是文件名、函数名或类名的一部分。
  • -m:只执行被特定标记(marker)装饰的测试用例。例如,pytest -m slow只执行被@pytest.mark.slow装饰的测试用例。
  • -x:一旦有任何一个测试用例执行失败,就停止当前线程的测试执行。
  • --maxfail=num:允许指定失败测试用例的最大数量,超过该数量后停止执行。
  • --reruns=num:对失败的测试用例进行重跑指定次数。需要安装pytest-rerunfailures插件模块。
  • --collect-only:只收集将要执行的测试用例,但不会实际执行它们。这可以用于查看哪些测试用例将被执行。
  • -lf-last-failed:只执行上次执行失败的测试。
  • -ff-failed-first:先执行上次失败的测试,然后再执行其他测试。
  • -html=报告文件:生成HTML格式的测试报告。需要安装pytest-html插件模块。
  • -junitxml=报告文件:生成XML格式的测试报告。需要安装pytest-xunitxml插件模块。
  • -json=报告文件:生成JSON格式的测试报告。需要安装pytest-json插件模块。
  • -cov=模块:生成指定模块的覆盖率报告。需要安装pytest-cov插件模块。

使用pytest.ini配置文件运行

命令运行最后还是会读取pytest.ini文件

所以最好的方法还是使用配置文件,这样每次运行就不需要指定参数了

但命令行的优先级更高,运行时会覆盖ini中的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[pytest]
# 默认命令行选项
addopts = -v --durations=10 --cov=src --cov-report=term-missing

# 测试路径和文件匹配
testpaths = 测试
python_files = test_*.py
python_classes = 测试*
python_functions = test_*

# 忽略的目录
norecursedirs = .venv .git __pycache__

# 自定义标记注册,和-m作用一样
markers =
smoke: marks tests as smoke
demo: marks tests as demo
API: marks tests as API for API automation tests

# 警告过滤
filterwarnings =
ignore::DeprecationWarning # 忽略DeprecationWarning告警
error::UserWarning # 将UserWarning告警转换为error

字段 作用描述 示例值
‘addopts’ 定义默认命令行选项(自动附加到 ‘pytest’ 命令后) ‘-v –tb=short –color=yes’
‘testpaths’ 指定 pytest 搜索测试文件的目录(默认所有目录) “unit_tests 测试
‘norecursedirs’ 排除搜索的目录(避免遍历不必要的路径) ‘.venv .git node_modules’
‘python_files’ 定义测试文件的命名模式(默认匹配 ‘test_*.py’ 或 ‘*_test.py’) ‘test_*.py check_*.py’
‘python_classes’ 定义测试类的命名模式(默认匹配 ‘Test*’) 测试套件
‘python_functions’ 定义测试函数的命名模式(默认匹配 ‘test_*’) ‘test_* check_*
‘markers’ 注册自定义标记(避免 ‘Unknown pytest.mark’ 警告) ‘slow:将测试标记为慢’
‘filterwarnings’ 控制 Python 警告的过滤行为 ‘ignore:.*deprecated.*:UserWarning’
‘log_cli’ 启用实时日志输出到控制台 ‘真’
‘timeoue’ 设置测试超时时间(需安装 ‘pytest-timeout’ 插件) 300 (单位:秒)

conftest.py配置文件

功能 描述
共享 Fixtures 定义全局可用的夹具(如数据库连接、临时目录),供多个测试文件复用。
加载插件 配置 pytest 插件(如 pytest-mockpytest-cov)或自定义插件。
定义钩子函数 自定义 pytest 行为(如修改测试收集逻辑、添加报告输出)。
配置作用域 不同层级的 conftest.py 控制不同范围的测试(目录级继承机制)。

加载机制

  • 文件名固定:必须命名为 conftest.py
  • 作用域:
  • 项目根目录的 conftest.py → 对所有测试生效。
  • 子目录中的 conftest.py → 仅对该目录及子目录下的测试生效。
  • 自动发现:pytest 运行时会自动加载所有符合作用域的 conftest.py

层级覆盖机制

在pytest中,conftest.py文件的设计理念与Helm工程中父级Values文件的层级覆盖机制非常相似,都是为了实现配置的集中管理灵活的层级覆盖,但是有着些许不同:

conftest.py中,是子文件优先级高于全局配置

  • 跨模块共享,多个测试文件可复用同一配置
  • 集中修改,只需改动一个文件
  • 目录层级继承,子目录可覆盖父级配置
  • 可通过不同 conftest.py 定义环境差异

断言机制assert

也就是assert关键字

1
2
3
def test_add():
result = 1 + 2
assert result == 3 # 验证计算结果是否为3
  • result == 3 → 断言通过,测试继续执行。
  • result ≠ 3 → 断言失败,抛出 AssertionError 并终止当前测试。

fixtrue装饰器(夹具

fixture 是 pytest 中的一个核心概念,用于定义测试运行前的准备工作和测试完成后的清理工作。fixture 可以是一个函数、一个类或者一个方法,并通过 @pytest.fixture 装饰器进行标记。

定义一个简单fixture

使用 @pytest.fixture 装饰器定义一个 fixture 函数。这个函数可以包含测试运行前的设置代码和测试完成后的清理代码。

1
2
3
4
5
6
@pytest.fixture(scope="session")
def database():
"""全局数据库连接(会话级)"""
conn = create_db_connection()
yield conn
conn.close()

fixture作用范围-scope参数

scope参数用来指定fixture的作用范围

function级

默认。每个测试函数调用时,都会创建一个新的fixture实例

在函数执行之前,function级fixture就会执行,并在该函数执行结束后销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest
@pytest.fixture()
def login():
print("打开浏览器")
a = "account"
return a
@pytest.fixture()
def logout():
print("关闭浏览器")

class Testlogin:
def test_001(self,login):
print("001传入login fixture")
assert login == "account"

class级

在每个测试类调用时,会创建一个fixture实例

如在下面这个例子中,fixture实例只会运行一次

如果创建了不同class,则会运行多次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pytest
# fixture作用域 scope = 'class'
@pytest.fixture(scope='class')
def login():
print("scope为class")
class TestLogin:
def test_1(self, login):
print("用例1")

def test_2(self, login):
print("用例2")

def test_3(self, login):
print("用例3")
if __name__ == '__main__':
pytest.main()

module级

在每个测试模块调用时,会创建一个fixture实例

在模块的所有测试开始之前创建一次,并在所有测试完成之后销毁一次(包括多个class的情况)

session级

在整个测试会话期间,只会创建一个fixture实例

范围是最大的,也被用于作为全局fixture

fixtrue自动调用-autouse参数

默认关闭

如果开启了这个参数,定义的fixture就会自动应用在当前目录与递归目录下的作用范围内

比如下面这个例子开启了autouse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest

# 定义一个自动应用的fixture
@pytest.fixture(autouse=True,scope=function)
def setup_and_teardown():
print("Setting up...")
# 这里可以是一些测试前的准备工作
# ...
yield # 将控制权交给测试代码
print("Tearing down...")
# 这里可以是一些测试后的清理工作

def test_example():
# 这个测试会自动使用上面的fixture
assert True

如果不开启,就需要在test_example中手动调用

1
2
3
def test_example(setup_and_teardown):
# 这个测试会自动使用上面的fixture
assert True

fixture参数化-params

当我们需要用不同的输入数据来测试同一个功能的时候,可以通过params传递多个参数,每个参数会生成一个测试用例。这样就不需要写多个测试函数,或者用循环来处理了。

用于测试同一功能在不同输入下的行为是否符合预期,减少重复代码。

  1. @pytest.fixture 中指定 params 参数(接受一个列表/元组)
  2. 在 Fixture 函数中通过 request.param 获取参数值
  3. 测试函数通过 Fixture 名称自动遍历所有参数
1
2
3
4
5
6
7
8
9
10
import pytest

# Fixture 提供 params 参数
@pytest.fixture(params=[1, 2, 3])
def number(request):
return request.param # 返回参数值

# 测试函数会生成 3 个用例(分别使用 1、2、3)
def test_is_positive(number):
assert number > 0

fixture参数别名-ids

params 中的每个参数提供一个 可读性更强的名称(默认显示参数值,复杂参数可读性差)。在测试报告中更清晰地标识不同用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest

@pytest.fixture(
params=[
{"username": "admin", "password": "secret"},
{"username": "guest", "password": "1234"}
],
ids=["Admin User", "Guest User"] # 自定义用例名称
)
def user(request):
return request.param

def test_login(user):
assert len(user["username"]) >= 4

用插件就能看到

fixture函数别名-name

  • 避免命名冲突:当 Fixture 函数名与测试函数名相同时,用 name 区分
  • 提高可读性:将底层工具函数命名为 _create_db,但暴露为 database Fixture
  • 动态生成 Fixture 名称:通过工厂函数批量创建 Fixture 时,用 name 参数赋予有意义的名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import pytest

# 定义 Fixture 并重命名
@pytest.fixture(name="answer")
def meaning_of_life(): # 原函数名隐藏
return 42

# 正确使用新名称
def test_correct(answer):
assert answer == 42 # ✅ 通过

# 错误使用原函数名
def test_wrong(meaning_of_life): # ❌ 触发 FixtureNotFoundError
assert meaning_of_life == 42

跳过测试用例skip、skipif

功能 @pytest.mark.skip @pytest.mark.skipif
跳过类型 无条件跳过 有条件跳过
常用场景 1. 尚未完成的测试2. 已知问题测试 1. 版本限制2. 环境依赖检测
参数要求 不需要条件参数 必须提供 condition 布尔表达式
标记位置 函数/类上方或代码内部 函数/类上方或代码内部
报告显示 显示为 skipped 显示为 skipped 并附带条件说明

使用pytest.mark.skip装饰器

  • 带mark的就是非动态的“标记”
  • 在装饰器参数中添加,用于跳过整个测试函数或类
  • 用于未实现功能或长期禁用的测试
  • 适合静态条件跳过
1
2
3
4
5
6
7
import pytest
@pytest.mark.skip(reason="功能尚未实现")
def test_new_feature():
assert False
def test_normal():
assert 1 + 1 == 2

用pytest.skip动态跳过

  • 用于测试函数内部逻辑
  • 可以在函数内任意位置使用
  • 用于运行时环境监测或动态依赖检查
  • 适合动态条件跳过
1
2
3
4
def test_database_connection():
if not check_database_available(): # 自定义检测函数
pytest.skip("数据库不可用")
# 正常测试逻辑...

使用pytest.mark.skipif条件跳过

1
2
3
4
5
6
7
8
import sys

@pytest.mark.skipif(
sys.version_info < (3, 8), # 条件:Python 版本低于 3.8
reason="需要 Python 3.8+ 的 walrus 运算符支持"
)
def test_walrus_operator():
assert (x := 5) == 5 # 海象运算符

使用pytest.skipif多条件判断

在下面这个代码,需要同时满足这两个条件

使用其他逻辑运算符可以构建诸如or等复杂条件判断

1
2
3
4
5
6
7
8
9
10
11
12
import os

@pytest.mark.skipif(
sys.platform != "linux",
reason="仅限 Linux 系统"
)
@pytest.mark.skipif(
not os.path.exists("/dev/special_device"),
reason="需要特殊硬件设备"
)
def test_linux_device():
assert run_device_check() == "OK"

将跳过标记赋值给变量

  • 进行代码复用
  • 统一管理相同条件的跳过描述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 定义通用跳过标记
skip_win = pytest.mark.skipif(
sys.platform == "win32",
reason="Windows 平台不支持的测试"
)
skip_dev = pytest.mark.skipif(
not os.path.exists("/dev/special"),
reason="需要开发板设备"
)

@skip_win
@skip_dev
def test_cross_platform_hardware():
...

缺少模块跳过

pytest.importorskip() 是处理 模块依赖管理 的核心工具

避免隐式依赖、提前检测

1
2
3
4
5
6
7
8
import pytest

# 测试需要 optional 依赖
def test_advanced_feature():
# 需要scipy模块,且版本大于1.8.0
scipy = pytest.importorskip("scipy", minversion="1.8.0")
result = scipy.special.erf(0.5)
assert abs(result - 0.5205) < 0.0001

自定义标记mark

  • 注册标记:

在pytest.ini中进行添加,以避免可能出现的warning

[pytest]
markers =
regular: 常规测试用例
model: 模型相关测试

  • 标记继承性:

当类被标记时,类中的所有测试方法都会继承该标记

标记的核心作用:选择性执行测试

当测试用例数量很多时,可以通过标记将测试分类,然后选择性地运行特定类别的测试。

1
2
3
4
5
6
7
8
9
10
11
@pytest.mark.regular  # 标记为 regular 类别的测试
def test_regular_a(): ...

@pytest.mark.model # 标记为 model 类别的测试
def test_model_b(): ...

然后在运行时通过-m指定参数:
# 只运行被标记为 regular 的测试
pytest -m regular
# 只运行被标记为 model 的测试
pytest -m model

pytest标记参数化生成测试用例

@pytest.mark.parametrize 是 pytest 中用于 批量生成测试用例 的核心装饰器,它的核心价值在于 用一组数据驱动多个测试场景

1. 避免重复代码

当多个测试用例逻辑相同,仅输入/输出不同时,无需重复编写多个函数。

2. 集中管理测试数据

测试数据与测试逻辑分离,便于维护和扩展。

3. 提高可读性

明确展示所有测试场景的输入和预期输出。

parametrize与fixture的区别

特性 @pytest.mark.parametrize @pytest.fixture(params=…)
主要用途 直接为测试函数提供多组输入参数 为多个测试函数共享同一组预处理数据
参数可见性 参数直接暴露在测试函数签名中 参数隐藏在 fixture 内部,测试函数只看到结果
参数传递方式 显式传递参数到测试函数 通过 fixture 返回值隐式传递
复用性 参数仅作用于当前测试函数 参数化后的 fixture 可被多个测试函数复用
数据预处理能力 只能传递原始数据 可在 fixture 内对参数进行复杂初始化处理
适用场景 简单的输入/输出组合验证 需要共享参数化资源(如数据库连接、配置文件等)
与测试逻辑的耦合度 测试逻辑与参数数据强耦合 测试逻辑与参数数据解耦
参数组合方式 支持笛卡尔积(叠加多个 parametrize 装饰器) 只能线性遍历参数
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
import pytest
# 使用parametrize处理
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(-1, 5, 4)
])
def test_add(a, b, expected):
assert a + b == expected

import pytest
# 使用fixture处理
@pytest.fixture(params=[
{'a':1, 'b':2, 'expected':3},
{'a':-1, 'b':5, 'expected':4}
])
def data(request):
return request.param

def test_add(data):
assert data['a'] + data['b'] == data['expected']


输出:
test_demo.py::test_add[1-2-3] PASSED
test_demo.py::test_add[-1-5-4] PASSED

test_demo.py::test_add[data0] PASSED
test_demo.py::test_add[data1] PASSED

可以看出 parametrize 能更直观展示参数内容,而 fixture 参数化需要结合 ids 参数才能优化显示

pytest标记失败@pytest.mark.xfail

核心用途

  • 预期失败的测试:标记已知但尚未修复的问题
  • 条件性跳过:当某些条件不满足时主动失败
  • 阶段性开发:标记未完成的测试用例

和skip的区别

特性 xfail skip
执行测试 ✔️ 会执行测试体 ❌ 完全不执行测试
失败处理 预期失败不算测试失败 直接标记为跳过
通过反馈 意外通过会标记为 XPASS 无反馈
结果统计 单独统计 XFAIL 和 XPASS 统计为 SKIPPED
适用场景 已知问题/未实现功能 环境不满足/临时禁用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import pytest

@pytest.mark.xfail(
strict=True, # 将意外通过视为测试失败
raises=ZeroDivisionError, # 指定预期异常类型
run=False # 标记但跳过执行
)
def test_advanced():
pass

@pytest.mark.xfail(reason="除法边界条件未处理")
def test_divide_zero():
assert 1 / 0 == 0 # 预期会失败

@pytest.mark.xfail(sys.platform == "win32",
reason="Windows 平台特性未实现")
def test_windows_specific():
assert os.path.exists("C:\\特殊目录") # 仅 Windows 会标记为预期失败

$ pytest -v
test_project/tests/xfail_test.py::test_divide_zero XFAIL (除法边界条件未处理) [ 50%]
test_project/tests/xfail_test.py::test_windows_specific XFAIL (Windows 平台特性未实现)


失败重试机制pytest-rerunfailures

pip install pytest-rerunfailures

因素 建议 示例
失败是否具有偶发性 ✅ 是 → 使用重试 网络超时
失败是否可自我修复 ✅ 是 → 使用重试 文件锁释放
是否涉及外部系统 ✅ 是 → 使用重试 数据库连接
是否是逻辑错误 ❌ 否 → 使用 xfail 算法错误
测试执行时间敏感 ❌ 否 → 谨慎使用重试 实时系统测试

重试参数

1
2
3
4
5
6
7
[pytest]
reruns = 2 # 全局设置测试失败后的自动重试次数
reruns_delay = 1 # 设置每次重试之间的等待时间(秒)

addopts = --reruns 2 --reruns-delay 1 # addopts默认命令行参数

@pytest.mark.flaky(reruns=5, reruns_delay=2)

适用重试的动态测试

  • 特点:依赖外部 API 的稳定性
  • 重试价值:网络抖动可能导致偶发失败
  • 最佳实践:重试次数建议 3-5 次,间隔 1-3 秒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
网络相关测试
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_download_file():
assert download_large_file().status == 'complete'

多线程/进程竞争
@pytest.mark.flaky(reruns=5)
def test_concurrent_access():
result = shared_resource.access()
assert not result.is_locked()

硬件接口调用
@pytest.mark.flaky(reruns=2)
def test_serial_port():
data = read_serial_port()
assert data == expected_packet

执行耗时参数durations

获取最慢的10个用例的执行耗时

1
pytest --durations=10

获取生成allure报告

环境是windows10

pytest会自己生成报告,但是都是json格式的,可读性比较差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 安装java
java -version
java version "1.8.0_441"
Java(TM) SE Runtime Environment (build 1.8.0_441-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.441-b07, mixed mode)

# 安装allure
在github中找到allure安装
allure --version
2.32.2

# 先执行一次,并添加参数
--alluredir=./temps --clean-alluredir

# 使用allure命令打开
allure serve ./allure-results
这条命令会打开一个web界面作为报告

CATALOG
  1. 1. 测试实践
    1. 1.1. 单元测试(验证函数逻辑)
    2. 1.2. 集成测试(验证多模块协作)
    3. 1.3. 端到端测试(模拟用户操作)
  2. 2. 运行pytest的方式
    1. 2.1. 使用pytest命令行运行
    2. 2.2. 使用pytest.ini配置文件运行
  3. 3. conftest.py配置文件
    1. 3.1. 加载机制
    2. 3.2. 层级覆盖机制
  4. 4. 断言机制assert
  5. 5. fixtrue装饰器(夹具)
    1. 5.1. 定义一个简单fixture
    2. 5.2. fixture作用范围-scope参数
      1. 5.2.1. function级
      2. 5.2.2. class级
      3. 5.2.3. module级
      4. 5.2.4. session级
    3. 5.3. fixtrue自动调用-autouse参数
    4. 5.4. fixture参数化-params
    5. 5.5. fixture参数别名-ids
    6. 5.6. fixture函数别名-name
  6. 6. 跳过测试用例skip、skipif
    1. 6.1. 使用pytest.mark.skip装饰器
    2. 6.2. 用pytest.skip动态跳过
    3. 6.3. 使用pytest.mark.skipif条件跳过
    4. 6.4. 使用pytest.skipif多条件判断
    5. 6.5. 将跳过标记赋值给变量
    6. 6.6. 缺少模块跳过
  7. 7. 自定义标记mark
    1. 7.1. 标记的核心作用:选择性执行测试
  8. 8. pytest标记参数化生成测试用例
    1. 8.1. parametrize与fixture的区别
  9. 9. pytest标记失败@pytest.mark.xfail
    1. 9.1. 和skip的区别
    2. 9.2. 失败重试机制pytest-rerunfailures
    3. 9.3. 重试参数
  10. 10. 执行耗时参数durations
  11. 11. 获取生成allure报告