一、Pytest 核心介绍
1. 什么是 Pytest
Pytest 是 Python 生态中最主流的自动化测试框架 ,专为编写和运行单元测试、接口测试、UI 测试等各类测试用例设计,相比 Python 内置的 unittest:
- ✅ 语法更简洁:无需继承类 / 写模板代码,一行断言即可完成测试;
- ✅ 功能更丰富:支持参数化、灵活的前后置操作、分类执行用例;
- ✅ 扩展性更强:上千个第三方插件覆盖报告、重试、并行执行等场景;
- ✅ 调试更友好:断言失败时输出详细对比信息,快速定位问题。
2. 适用场景
- 单元测试:验证单个函数 / 类的逻辑(如计算函数、数据处理类);
- 接口自动化:结合
requests测试 HTTP/HTTPS 接口(如登录、下单接口); - UI 自动化:结合
selenium/appium测试 Web/APP 页面; - 回归测试:批量执行核心用例,保障代码迭代后功能稳定。
3. 核心特点
✅ 极简的语法
无需继承类、写繁琐的模板代码,一行断言就能完成基础测试,新手上手成本极低:
python
# 最简单的 Pytest 用例
def test_add():
assert 1 + 2 == 3 # 断言成立则用例通过,不成立则失败
✅ 强大的自动发现机制
遵循简单的命名约定,Pytest 会自动扫描并识别测试用例,无需手动配置:
- 测试文件:以
test_开头(如test_demo.py)或_test结尾(如demo_test.py) - 测试类:以
Test开头,且无__init__方法(如TestLogin) - 测试函数 / 方法:以
test_开头(如test_login_success())
✅ 丰富的内置功能
- 灵活的断言 :支持自定义失败提示,调试更友好(如
assert a == b, "a和b不相等") - 参数化:批量生成测试用例,避免重复代码
- Fixture 固件 :替代传统
setup/teardown,实现灵活的前置 / 后置操作(如初始化数据库、启动浏览器) - 标记功能:给用例分类(如冒烟用例、回归用例),按需执行
- 失败重试 :结合插件(
pytest-rerunfailures)实现失败用例自动重试 - 测试报告 :结合插件(
pytest-html)生成可视化 HTML 报告
✅ 高度可扩展
支持上千个第三方插件,覆盖各类测试场景:
- 接口测试:
pytest-requests - UI 测试:
pytest-selenium - 并行执行:
pytest-xdist(提升测试效率) - 用例排序:
pytest-ordering
✅ 兼容原有测试代码
可直接运行 unittest 编写的测试用例,无需修改代码,迁移成本低。
4. 核心概念
✅ 断言(Assert)
断言是判断用例是否通过的核心,Pytest 对 Python 原生 assert 做了增强,失败时会输出详细的对比信息:
python
def test_user_info():
user = {"name": "张三", "age": 20}
# 带自定义提示的断言
assert user["age"] == 18, f"预期年龄18,实际{user['age']}"
运行失败时会显示:AssertionError: 预期年龄18,实际20
✅ Fixture(固件)
Fixture 是 Pytest 最核心的特性,用于实现测试用例的前置 / 后置操作,支持不同作用域(函数、类、模块、会话):
python
import pytest
# 定义 Fixture(函数级,每个用例执行前/后运行)
@pytest.fixture
def init_data():
print("\n前置操作:初始化测试数据")
data = [1, 2, 3]
yield data # 返回数据给用例,yield 后是后置操作
print("\n后置操作:清理测试数据")
# 使用 Fixture(直接传函数名)
def test_use_data(init_data):
assert len(init_data) == 3
✅ 标记(Mark)
通过 @pytest.mark.xxx 给用例打标签,实现分类执行:
python
import pytest
# 标记为冒烟用例
@pytest.mark.smoke
def test_login():
assert True
# 标记为用户模块用例
@pytest.mark.user
def test_user_list():
assert True
执行指定标记的用例:
bash
pytest -m smoke # 只执行冒烟用例
✅ 参数化(Parametrize)
批量生成测试用例,避免重复编写代码:
python
import pytest
# 参数化:(参数名, 参数列表)
@pytest.mark.parametrize("a, b, expected", [(1,2,3), (4,5,9), (10,20,30)])
def test_add(a, b, expected):
assert a + b == expected
上述代码会自动生成 3 个测试用例,分别验证不同的加法场景。
5. 相关单词
| 英文单词 / 短语 | 音标 | 中文含义 | 常用场景 |
|---|---|---|---|
| pytest | /ˈpaɪtest/ | Python 测试框架 | 单元测试、接口自动化 |
| test | /test/ | 测试 | 测试用例、测试函数 |
| case | /keɪs/ | 用例 | test case 测试用例 |
| function | /ˈfʌŋkʃn/ | 函数 | 测试函数、函数级用例 |
| class | /klɑːs/ | 类 | 测试类 TestClass |
| module | /ˈmɒdjuːl/ | 模块 | 测试文件、模块级 fixture |
| session | /ˈseʃn/ | 会话 | 一次完整测试运行周期 |
| fixture | /ˈfɪkstʃə®/ | 夹具 | 前置 / 后置、数据准备 |
| scope | /skəʊp/ | 作用域 | fixture 作用域 |
| setup | /ˈsetʌp/ | 初始化、前置 | 测试前准备环境 |
| teardown | /ˈtɪədaʊn/ | 清理、后置 | 测试后释放资源 |
| conftest | /ˈkɒnftest/ | 全局配置 | conftest.py 共享 fixture |
| mark | /mɑːk/ | 标记 | @pytest.mark 自定义标记 |
| skip | /skɪp/ | 跳过 | 跳过当前用例 |
| skipif | /ˈskɪpɪf/ | 条件跳过 | 满足条件则跳过 |
| xfail | /ˈeksfeɪl/ | 预期失败 | 预期会失败的用例 |
| parametrize | /pəˈræmətrˌaɪz/ | 参数化 | 多组数据驱动测试 |
| assert | /əˈsɜːt/ | 断言 | 校验结果是否符合预期 |
| equal | /ˈiːkwəl/ | 相等 | assert a == b |
| raise | /reɪz/ | 抛出 | 断言抛出异常 |
| exception | /ɪkˈsepʃn/ | 异常 | pytest.raises 捕获异常 |
| plugin | /ˈplʌɡɪn/ | 插件 | pytest 扩展功能 |
| hook | /hʊk/ | 钩子 | 自定义测试流程 |
| discover | /dɪˈskʌvə®/ | 发现 | 自动识别测试用例 |
| run | /rʌn/ | 运行 | 执行测试 |
| pass | /pɑːs/ | 通过 | 用例执行成功 |
| fail | /feɪl/ | 失败 | 用例执行失败 |
| warning | /ˈwɔːnɪŋ/ | 警告 | 测试警告信息 |
| report | /rɪˈpɔːt/ | 报告 | 生成测试报告 |
| coverage | /ˈkʌvərɪdʒ/ | 覆盖率 | 代码测试覆盖率 |
| allure | /əˈlʊə®/ | 测试报告框架 | 生成美观可视化报告 |
| allure-pytest | /əˈlʊə ˈpaɪtest/ | allure 插件 | pytest 集成 allure |
| feature | /ˈfiːtʃə®/ | 功能 | @allure.feature 标记模块 |
| story | /ˈstɔːri/ | 子功能 | @allure.story 标记子模块 |
| step | /step/ | 步骤 | @allure.step 标记测试步骤 |
| title | /ˈtaɪtl/ | 标题 | @allure.title 自定义用例标题 |
| severity | /sɪˈverəti/ | 优先级 | 标记用例严重等级 |
| attachment | /əˈtætʃmənt/ | 附件 | 添加截图、日志、文件 |
| link | /lɪŋk/ | 链接 | 关联需求 / 缺陷地址 |
二、Pytest 基础用法
1. 环境准备
1.1. 安装 Pytest
bash
# 推荐安装稳定版本
pip install pytest==9.0.2
# 验证安装结果
pytest --version
执行结果:
终端输出 pytest 9.0.2 即安装成功;若提示 pytest: 未找到命令,需检查 Python 环境变量(Windows 需勾选 "Add Python to PATH",Mac/Linux 可执行 python -m pytest --version)。
1.2. 核心命名约定(必遵守)
Pytest 会自动扫描符合以下规则的文件 / 类 / 函数,不遵守则收集不到用例:
| 类型 | 规则 | 正确示例 | 错误示例 |
|---|---|---|---|
| 测试文件 | 以 test_ 开头 或 _test 结尾 |
test_calculate.py |
calculate_test.py(✅)、calculate.py(❌) |
| 测试类 | 以 Test 开头 + 无 __init__ 方法 |
TestCalculate |
test_calculate(❌)、TestCalc()(含__init__,❌) |
| 测试函数 / 方法 | 以 test_ 开头 |
test_add()、test_sub() |
add_test()(❌)、TestAdd()(❌) |
2. 快速入门
2.1. 编写完整用例文件
新建项目目录,创建 test_calculate.py,内容如下:
python
"""测试计算器功能的基础用例"""
# 定义待测试的普通函数(模拟实际项目中的业务逻辑)
def add(a, b):
"""加法函数"""
return a + b
def sub(a, b):
"""减法函数"""
return a - b
# 测试函数:验证加法功能
def test_add():
# 步骤1:准备测试数据
num1 = 10
num2 = 20
expected = 30
# 步骤2:执行待测试函数
actual = add(num1, num2)
# 步骤3:断言结果是否符合预期(核心)
assert actual == expected, f"加法计算错误!预期{expected},实际{actual}"
# 测试类:组织多个相关测试方法
class TestCalculate:
# 测试减法功能(正常场景)
def test_sub_normal(self):
actual = sub(50, 20)
assert actual == 30, f"减法计算错误!预期30,实际{actual}"
# 测试减法功能(边界场景:负数)
def test_sub_negative(self):
actual = sub(10, 30)
assert actual == -20, f"负数减法错误!预期-20,实际{actual}"
2.2. 运行用例
bash
# 方式1:运行当前目录所有用例(显示详细结果)
pytest -v
# 方式2:指定文件运行(精准执行,推荐)
pytest test_calculate.py -v -s
参数说明:-v 显示用例名称和结果,-s 显示用例中的 print 输出(若有)。
2.3. 执行结果解读
cmd
collected 3 items
test_calculate.py::test_add PASSED
test_calculate.py::TestCalculate::test_sub_normal PASSED
test_calculate.py::TestCalculate::test_sub_negative PASSED
========================== 3 passed in 0.01s ==========================
collected 3 items:Pytest 识别到 3 个可用用例;PASSED:用例执行通过;若断言失败,会显示FAILED+ 详细错误信息;0.01s:用例执行总耗时。
2.4. 模拟断言失败场景
修改 test_add 中的预期值:
python
def test_add():
num1 = 10
num2 = 20
expected = 31 # 故意写错,模拟失败
actual = add(num1, num2)
assert actual == expected, f"加法计算错误!预期{expected},实际{actual}"
重新运行 pytest test_calculate.py -v,结果如下:
cmd
test_calculate.py::test_add FAILED
test_calculate.py::TestCalculate::test_sub_normal PASSED
test_calculate.py::TestCalculate::test_sub_negative PASSED
========================== FAILURES ==========================
___________________________ test_add ___________________________
def test_add():
num1 = 10
num2 = 20
expected = 31
actual = add(num1, num2)
> assert actual == expected, f"加法计算错误!预期{expected},实际{actual}"
E AssertionError: 加法计算错误!预期31,实际30
E assert 30 == 31
test_calculate.py:19: AssertionError
========================== 1 failed, 2 passed in 0.02s ==========================
关键信息:
AssertionError:断言失败类型;assert 30 == 31:明确显示预期值和实际值的对比;test_calculate.py:19:定位到失败代码的行号。
3. 核心语法:断言(Assert)
Pytest 增强了 Python 原生断言,支持自定义提示 和复杂场景判断,以下是实战中最常用的断言类型:
(1)基础断言(附详细示例)
python
"""断言实战示例"""
import pytest
def test_assert_basic():
# 1. 相等/不相等判断(最常用)
a = "hello"
b = "world"
assert a != b, f"a和b不应相等!a={a}, b={b}" # 成功
# 2. 包含/不包含判断(字符串/列表/字典)
str1 = "pytest教程"
assert "pytest" in str1, f"str1不包含'pytest'!str1={str1}" # 成功
list1 = [1,2,3]
assert 4 not in list1, f"list1不应包含4!list1={list1}" # 成功
# 3. 布尔判断(True/False)
flag = True
assert flag, "flag应为True!" # 成功
# 4. 数值大小判断
num = 99
assert num > 90, f"num应大于90!num={num}" # 成功
def test_assert_dict():
# 复杂断言:字典内容验证(接口测试常用)
actual_response = {
"code": 200,
"msg": "登录成功",
"data": {"user_id": 1001, "username": "zhangsan"}
}
# 验证状态码
assert actual_response["code"] == 200, "接口返回码错误"
# 验证data中的user_id
assert actual_response["data"]["user_id"] == 1001, "用户ID错误"
运行 pytest test_assert.py -v,结果全部 PASSED;若修改 actual_response["code"] = 500,则会提示 AssertionError: 接口返回码错误。
4. 命令行参数
4.1. 常用参数
| 参数 | 作用 | 实战示例 | 示例说明 |
|---|---|---|---|
-v |
显示详细用例名称和结果 | pytest -v |
能看到每个用例的完整路径(如 test_assert.py::test_assert_basic) |
-s |
显示用例中的 print / 日志输出 | pytest -s |
调试时查看变量值、执行步骤 |
-x |
第一个用例失败则停止执行 | pytest -x |
快速定位首个失败用例,节省调试时间 |
-k |
按关键字筛选用例 | pytest -k "add or sub" |
只执行名称含 add 或 sub 的用例 |
--tb=short |
简化失败时的报错信息(只显示关键行) | pytest --tb=short |
避免冗长报错日志,快速看失败原因 |
-m |
按标记筛选用例(进阶) | pytest -m smoke |
只执行标记为 smoke 的冒烟用例 |
实战示例:
运行 pytest -vs -k "add",结果如下:
cmd
collected 3 items / 2 deselected / 1 selected
test_calculate.py::test_add PASSED
========================== 1 passed, 2 deselected in 0.01s ==========================
1 selected:选中 1 个含add的用例;2 deselected:排除 2 个不含add的用例。
4.2. 全部参数
| 分类 | 参数名 | 中文说明 |
|---|---|---|
| 位置参数 | file_or_dir | 指定要运行的测试文件或目录(可传多个) |
| 通用参数 | -k EXPRESSION | 仅运行匹配给定子字符串表达式的测试。表达式是可求值的 Python 表达式,所有名称会按子字符串匹配测试名及其父类(大小写不敏感)。示例:-k 'test_method or test_other' 匹配含该关键词的测试,-k 'not test_method' 排除匹配项 |
| -m MARKEXPR | 仅运行匹配给定标记表达式的测试。示例:-m 'mark1 and not mark2' | |
| --markers | 显示所有标记(内置、插件、项目自定义) | |
| -x, --exitfirst | 遇到第一个错误 / 失败用例时立即退出 | |
| --maxfail=num | 累计失败 / 错误数达到 num 时退出 | |
| --strict-config | 启用 strict_config 选项(解析配置文件时遇到警告直接报错) | |
| --strict-markers | 启用 strict_markers 选项(未注册的标记直接报错) | |
| --strict | 启用所有严格模式(包含 strict_config、strict_markers 等) | |
| --fixtures, --funcargs | 显示可用的夹具(按插件加载顺序排序,以_开头的夹具仅在 - v 时显示) | |
| --fixtures-per-test | 显示每个测试用例关联的夹具 | |
| --pdb | 在测试错误 / 键盘中断时启动交互式 Python 调试器 | |
| --pdbcls=modulename:classname | 指定自定义调试器(示例:--pdbcls=IPython.terminal.debugger:TerminalPdb) | |
| --trace | 运行每个测试用例时立即触发断点 | |
| --capture=method | 每个测试的输出捕获方式:fd|sys|no|tee-sys | |
| -s | --capture=no 的简写(禁用输出捕获,显示 print 等输出) | |
| --runxfail | 将 xfail 标记的测试按普通测试执行(不标记为 xfail) | |
| --lf, --last-failed | 仅重跑上次运行失败的测试(无失败则运行全部) | |
| --ff, --failed-first | 运行所有测试,但先运行上次失败的测试(可能打乱夹具的 setup/teardown 顺序) | |
| --nf, --new-first | 先运行新文件中的测试,其余按文件修改时间排序 | |
| --cache-show=CACHESHOW | 显示缓存内容,不执行收集 / 测试(默认 glob:*) | |
| --cache-clear | 测试开始时清空所有缓存内容 | |
| --lfnf, --last-failed-no-failures={all,none} | 配合 --lf 使用:无历史失败时,all(运行全部,默认)/none(仅提示并退出) | |
| --sw, --stepwise | 测试失败时退出,下次运行从失败的用例继续 | |
| --sw-skip, --stepwise-skip | 忽略第一个失败用例,在第二个失败时停止(自动启用 --stepwise) | |
| --sw-reset, --stepwise-reset | 重置 stepwise 状态,重新开始流程(自动启用 --stepwise) | |
| 报告参数 | --durations=N | 显示 N 个最慢的夹具 / 测试执行时长(N=0 显示全部) |
| --durations-min=N | 纳入 "最慢列表" 的最小时长(秒),默认 0.005(-vv 时为 0.0) | |
| -v, --verbose | 提高输出详细程度(显示每个用例的执行结果) | |
| --no-header | 禁用输出头部信息 | |
| --no-summary | 禁用输出总结信息 | |
| --no-fold-skipped | 简短总结中不折叠跳过的测试 | |
| --force-short-summary | 强制输出精简版总结(忽略详细程度级别) | |
| -q, --quiet | 降低输出详细程度(仅显示关键结果) | |
| --verbosity=VERBOSE | 设置详细程度,默认 0 | |
| -r chars | 显示额外的测试总结信息,字符含义:f (失败)、E (错误)、s (跳过)、x (预期失败)、X (预期失败但通过)、p (通过)、P (通过且有输出)、a (除 p/P 外全部)、A (全部);w (警告,默认启用);N (重置列表);默认:fE | |
| --disable-warnings, --disable-pytest-warnings | 禁用警告总结输出 | |
| -l, --showlocals | 在回溯信息中显示局部变量(默认禁用) | |
| --no-showlocals | 隐藏回溯中的局部变量(抵消 addopts 中的 --showlocals) | |
| --tb=style | 回溯信息打印模式:auto|long|short|line|native|no | |
| --xfail-tb | 为 xfail 用例显示回溯(只要 --tb≠no) | |
| --show-capture={no,stdout,stderr,log,all} | 控制失败测试中捕获的输出显示方式,默认 all | |
| --full-trace | 不截断任何回溯信息(默认会截断) | |
| --color=color | 终端输出颜色:yes|no|auto | |
| --code-highlight={yes,no} | 是否高亮代码(仅在 --color 启用时生效),默认 yes | |
| --pastebin=mode | 将失败 / 全部测试信息发送到bpaste.net粘贴板(mode:failed|all) | |
| --junitxml, --junit-xml=path | 在指定路径生成 JUnit XML 格式的报告 | |
| --junitprefix, --junit-prefix=str | 为 JUnit XML 输出中的类名添加前缀 | |
| --html=path | 在指定路径生成 HTML 测试报告 | |
| --self-contained-html | 生成包含所有样式 / 脚本 / 图片的独立 HTML 报告(CSP 限制环境可能无法渲染) | |
| --css=path | 将指定 CSS 文件内容追加到报告样式中 | |
| Python 警告参数 | -W, --pythonwarnings PYTHONWARNINGS | 设置要报告的警告类型(同 Python 的 - W 参数) |
| 用例收集参数 | --collect-only, --co | 仅收集测试用例,不执行 |
| --pyargs | 尝试将所有参数解析为 Python 包 | |
| --ignore=path | 收集时忽略指定路径(可多次使用) | |
| --ignore-glob=path | 收集时忽略匹配通配符的路径(可多次使用) | |
| --deselect=nodeid_prefix | 收集时取消选中指定节点 ID 前缀的用例(可多次使用) | |
| --confcutdir=dir | 仅加载指定目录下的 conftest.py | |
| --noconftest | 不加载任何 conftest.py 文件 | |
| --keep-duplicates | 保留重复的测试用例 | |
| --collect-in-virtualenv | 不忽略本地虚拟环境目录中的测试 | |
| --continue-on-collection-errors | 即使收集阶段出错,仍强制执行测试 | |
| --import-mode={prepend,append,importlib} | 导入测试模块 /conftest 时修改 sys.path 的方式:prepend(前置,默认)|append(后置)|importlib | |
| --doctest-modules | 运行所有.py 模块中的 doctest | |
| --doctest-report={none,cdiff,ndiff,udiff,only_first_failure} | doctest 失败时的 diff 输出格式 | |
| --doctest-glob=pat | doctest 文件匹配通配符,默认 test*.txt | |
| --doctest-ignore-import-errors | 忽略 doctest 收集阶段的导入错误 | |
| --doctest-continue-on-failure | 单个 doctest 中第一个失败后继续执行 | |
| 调试 & 配置参数 | -c, --config-file FILE | 从指定文件加载配置(不使用默认配置文件) |
| --rootdir=ROOTDIR | 定义测试根目录(支持相对路径、绝对路径、环境变量) | |
| --basetemp=dir | 测试运行的基础临时目录(警告:目录存在会被删除) | |
| -V, --version | 显示 pytest 版本和插件信息(两次 - V 显示更多插件详情) | |
| -h, --help | 显示帮助信息和配置说明 | |
| -p name | 提前加载指定插件(可多次使用);前缀 no: 表示禁用插件(示例:-p no:doctest) | |
| --disable-plugin-autoload | 禁用插件自动加载(仅加载 - p 或 PYTEST_PLUGINS 指定的插件) | |
| --trace-config | 追踪 conftest.py 文件的加载过程 | |
| --debug=DEBUG_FILE_NAME | 将内部调试信息存储到日志文件(默认 pytestdebug.log,覆盖写入) | |
| -o, --override-ini OVERRIDE_INI | 覆盖配置选项(格式:option=value,示例:-o strict_xfail=True -o cache_dir=cache) | |
| --assert=MODE | 控制断言调试工具:plain(无调试)、rewrite(默认,重写 assert 语句) | |
| --setup-only | 仅初始化夹具,不执行测试用例 | |
| --setup-show | 执行测试时显示夹具的初始化过程 | |
| --setup-plan | 显示要执行的夹具和测试,但不实际执行 | |
| 日志参数 | --log-level=LEVEL | 捕获 / 显示的日志级别(默认继承根处理器的 WARNING) |
| --log-format=LOG_FORMAT | 日志模块使用的格式 | |
| --log-date-format=LOG_DATE_FORMAT | 日志模块使用的日期格式 | |
| --log-cli-level=LOG_CLI_LEVEL | 终端日志显示级别 | |
| --log-cli-format=LOG_CLI_FORMAT | 终端日志使用的格式 | |
| --log-cli-date-format=LOG_CLI_DATE_FORMAT | 终端日志使用的日期格式 | |
| --log-file=LOG_FILE | 日志写入的文件路径 | |
| --log-file-mode={w,a} | 日志文件打开模式(w:覆盖,a:追加) | |
| --log-file-level=LOG_FILE_LEVEL | 日志文件的日志级别 | |
| --log-file-format=LOG_FILE_FORMAT | 日志文件使用的格式 | |
| --log-file-date-format=LOG_FILE_DATE_FORMAT | 日志文件使用的日期格式 | |
| --log-auto-indent=LOG_AUTO_INDENT | 自动缩进多行日志消息(值:true|on/false|off / 整数) | |
| --log-disable=LOGGER_DISABLE | 禁用指定名称的日志器(可多次使用) | |
| 异步 IO 参数 | --asyncio-mode=MODE | asyncio 模式:auto(自动处理所有异步函数,默认)、strict(禁用自动处理) |
| --asyncio-debug | 为默认事件循环启用 asyncio 调试模式 | |
| 覆盖率参数 | --cov=SOURCE | 指定要测量覆盖率的路径 / 包名(可多次使用);--cov = 表示不过滤,记录所有代码 |
| --cov-reset | 重置已累积的 cov 源配置 | |
| --cov-report=TYPE | 生成的覆盖率报告类型:term|term-missing|annotate|html|xml|json|markdown 等(可指定输出路径,示例:html:./cov_html) | |
| --cov-config=PATH | 覆盖率配置文件路径,默认.coveragerc | |
| --no-cov-on-fail | 测试失败时不生成覆盖率报告,默认 False | |
| --no-cov | 完全禁用覆盖率报告(调试时用),默认 False | |
| --cov-fail-under=MIN | 总覆盖率低于 MIN 时测试失败 | |
| --cov-append | 追加覆盖率数据(不删除原有数据),默认 False | |
| --cov-branch | 启用分支覆盖率统计 | |
| --cov-precision=COV_PRECISION | 覆盖报告的精度(小数位数) | |
| --cov-context=CONTEXT | 使用的动态上下文,目前仅支持 "test" | |
| 元数据参数 | --metadata=key value | 添加额外元数据(键值对) |
| --metadata-from-json=METADATA_FROM_JSON | 从 JSON 字符串添加额外元数据 | |
| --metadata-from-json-file=METADATA_FROM_JSON_FILE | 从 JSON 文件添加额外元数据 | |
| 失败重跑参数 | --force-reruns=FORCE_RERUNS | 强制所有测试重跑指定次数(忽略标记) |
| --only-rerun=ONLY_RERUN | 仅重跑匹配正则的错误(可多次使用) | |
| --reruns=RERUNS | 失败测试的重跑次数,默认 0 | |
| --reruns-delay=RERUNS_DELAY | 重跑间隔时间(秒) | |
| --rerun-except=RERUN_EXCEPT | 不重跑匹配正则的错误(可多次使用) | |
| --fail-on-flaky | 若不稳定测试重跑后通过,测试整体以退出码 7 失败 | |
| 分布式测试参数 | -n, --numprocesses numprocesses | --dist=load --tx=NUM*popen 的简写;值为 logical/auto 时自动检测 CPU 数;与 --pdb 同用时强制为 0(禁用) |
| --maxprocesses=maxprocesses | 限制 --numprocesses=auto/logical 时的最大工作进程数 | |
| --max-worker-restart=MAXWORKERRESTART | 崩溃后可重启的最大工作进程数(0 禁用) | |
| --dist=distmode | 测试分发模式:each(所有环境运行每个测试)、load(负载均衡)、loadscope(按作用域分组)、loadfile(按文件分组)、loadgroup(按 xdist_group 标记分组)、worksteal(工作窃取)、no(默认,进程内运行) | |
| --loadscope-reorder | loadscope 模式下按作用域用例数重排测试(提升并行度,可能打乱顺序) | |
| --no-loadscope-reorder | 禁用 loadscope 重排(保留测试顺序) | |
| --tx=xspec | 添加测试执行环境(示例:--tx popen//python=python2.5、--tx ssh=user@xxx//chdir=testcache) | |
| --px=xspec | 添加代理网关(示例:--px id=proxy//socket=ip:port --tx 5*popen//via=proxy) | |
| -d | --dist=load 的简写(负载均衡分发测试) | |
| --rsyncdir=DIR | 添加要同步到远程 tx 节点的目录 | |
| --rsyncignore=GLOB | 添加 rsync 同步时忽略的通配符规则 | |
| --testrunuid=TESTRUNUID | 为所有工作进程设置统一的 testrun_uid 夹具值(默认自动生成唯一值) | |
| --maxschedchunk=MAXSCHEDCHUNK | load 模式下单次调度的最大测试数(1:逐个调度,适合慢测试;默认不限) | |
| -f, --looponfail | 子进程中运行测试:监听文件修改,重跑失败用例直到全部通过 |
补充说明
- 表格中 "可多次使用" 表示参数可重复传入(如
--ignore=path1 --ignore=path2); - 部分参数依赖第三方插件(如
--html依赖 pytest-html,--reruns依赖 pytest-rerunfailures,-n依赖 pytest-xdist); - 配置文件参数(如 markers、addopts 等)是 pytest.ini/pyproject.toml 等配置文件中的选项,非命令行参数,未列入表格;
- 环境变量(如 PYTEST_ADDOPTS、CI 等)是系统级配置,未列入表格。
5. 全局配置文件:pytest.ini
5.1. 常用参数
放在项目根目录,替代重复的命令行参数,优先于命令行生效,示例如下:
ini
[pytest]
; 全局默认参数(每次运行pytest自动带上-vs)
addopts = -vs
; 测试用例存放目录(只扫描tests文件夹)
testpaths = ./tests
; 测试文件命名规则(必须匹配实际文件名)
python_files = test_*.py
; 测试类命名规则(必须匹配实际类名)
python_classes = Test*
; 测试函数命名规则(必须匹配实际函数名)
python_functions = test_*
; 注册自定义标记(进阶用,避免警告)
markers =
smoke: 冒烟用例(核心流程)
normal: 普通用例
exception: 异常场景用例
生效验证:
配置后直接运行 pytest,无需加 -vs,终端会自动显示详细结果和 print 输出;若 tests 目录下有 test_demo.py,会自动扫描,无需指定文件名。
5.2. 全部参数
以下是 pytest 配置文件(pytest.ini/pyproject.toml/tox.ini/setup.cfg 等)专属参数:
| 配置项名称 | 类型 | 中文说明 |
|---|---|---|
| markers | 行列表(list) | 为测试函数注册自定义标记(示例:markers = smoke: 冒烟用例 \nnormal: 普通用例) |
| empty_parameter_set_mark | 字符串(str) | 空参数集的默认标记 |
| strict_config | 布尔(bool) | 解析配置文件的 pytest 段时,遇到警告直接抛出错误(严格模式) |
| strict_markers | 布尔(bool) | 未在 markers 中注册的标记会直接报错(严格校验标记) |
| strict | 布尔(bool) | 启用所有严格模式(包含 strict_config、strict_markers、strict_xfail、strict_parametrization_ids) |
| filterwarnings | 行列表(list) | 每行指定一个 warnings.filterwarnings 的过滤规则,在 - W/--pythonwarnings 之后处理 |
| norecursedirs | 参数列表(args) | 递归查找测试时需要忽略的目录模式(示例:norecursedirs = .git venv pycache) |
| testpaths | 参数列表(args) | 未指定文件 / 目录时,用于搜索测试的目录(示例:testpaths = tests/unit tests/integration) |
| collect_imported_tests | 布尔(bool) | 是否收集 testpaths 外导入模块中的测试用例 |
| consider_namespace_packages | 布尔(bool) | 导入模块时考虑命名空间包 |
| usefixtures | 参数列表(args) | 项目默认使用的夹具列表(所有测试自动应用这些夹具) |
| python_files | 参数列表(args) | Python 测试模块发现的通配符模式(示例:python_files = test_*.py *_test.py) |
| python_classes | 参数列表(args) | Python 测试类发现的前缀 / 通配符(示例:python_classes = Test*) |
| python_functions | 参数列表(args) | Python 测试函数 / 方法发现的前缀 / 通配符(示例:python_functions = test_*) |
| disable_test_id_escaping_and_forfeit_all_rights_to_community_support | 布尔(bool) | 禁用非 ASCII 字符的字符串转义(可能导致副作用,风险自负) |
| strict_parametrization_ids | 布尔(bool) | 检测到非唯一的参数集 ID 时抛出错误 |
| console_output_style | 字符串(str) | 控制台输出样式:classic(经典)、progress(百分比进度)、count(计数)、progress-even-when-capture-no(强制显示进度) |
| verbosity_test_cases | 字符串(str) | 测试用例执行的详细程度(覆盖主 verbosity 级别,值越高输出越详细) |
| strict_xfail | 布尔(bool) | xfail 标记未显式指定 strict 参数时的默认值(默认 False,别名:xfail_strict) |
| tmp_path_retention_count | 字符串(str) | 按 tmp_path_retention_policy 保留的 tmp_path 目录数量 |
| tmp_path_retention_policy | 字符串(str) | 控制 tmp_path 夹具创建的目录保留策略:all(全部)、failed(仅失败用例)、none(不保留) |
| enable_assertion_pass_hook | 布尔(bool) | 启用 pytest_assertion_pass 钩子(需删除已生成的 pyc 缓存文件) |
| truncation_limit_lines | 字符串(str) | 触发回溯信息截断的行数阈值 |
| truncation_limit_chars | 字符串(str) | 触发回溯信息截断的字符数阈值 |
| verbosity_assertions | 字符串(str) | 断言的详细程度(覆盖主 verbosity 级别,值越高断言失败时解释越详细) |
| junit_suite_name | 字符串(str) | JUnit 报告的测试套件名称 |
| junit_logging | 字符串(str) | 将捕获的日志写入 JUnit 报告:no|log|system-out|system-err|out-err|all |
| junit_log_passing_tests | 布尔(bool) | 为通过的测试捕获日志并写入 JUnit 报告 |
| junit_duration_report | 字符串(str) | 报告的时长类型:total(总时长)、call(调用时长) |
| junit_family | 字符串(str) | 生成的 XML 遵循的 schema:legacy|xunit1|xunit2 |
| doctest_optionflags | 参数列表(args) | doctest 的选项标志 |
| doctest_encoding | 字符串(str) | doctest 文件使用的编码 |
| cache_dir | 字符串(str) | 缓存目录路径 |
| log_level | 字符串(str) | --log-level 的默认值 |
| log_format | 字符串(str) | --log-format 的默认值 |
| log_date_format | 字符串(str) | --log-date-format 的默认值 |
| log_cli | 布尔(bool) | 启用测试运行时的实时日志显示(live logging) |
| log_cli_level | 字符串(str) | --log-cli-level 的默认值 |
| log_cli_format | 字符串(str) | --log-cli-format 的默认值 |
| log_cli_date_format | 字符串(str) | --log-cli-date-format 的默认值 |
| log_file | 字符串(str) | --log-file 的默认值 |
| log_file_mode | 字符串(str) | --log-file-mode 的默认值 |
| log_file_level | 字符串(str) | --log-file-level 的默认值 |
| log_file_format | 字符串(str) | --log-file-format 的默认值 |
| log_file_date_format | 字符串(str) | --log-file-date-format 的默认值 |
| log_auto_indent | 字符串(str) | --log-auto-indent 的默认值 |
| faulthandler_timeout | 字符串(str) | 测试超时(秒)时转储所有线程的回溯信息 |
| faulthandler_exit_on_timeout | 布尔(bool) | 测试超时时退出测试进程 |
| verbosity_subtests | 字符串(str) | 子测试的详细程度(值越高,通过的子测试也会输出信息;失败的子测试始终报告) |
| addopts | 参数列表(args) | 额外的命令行参数(运行 pytest 时自动追加,示例:addopts = -vs --html=report.html) |
| minversion | 字符串(str) | 项目要求的最低 pytest 版本(示例:minversion = 7.0) |
| pythonpath | 路径列表(paths) | 添加到 sys.path 的路径(多个路径用逗号 / 换行分隔) |
| required_plugins | 参数列表(args) | pytest 运行必须存在的插件(缺失则报错,示例:required_plugins = pytest-html>=3.0 pytest-rerunfailures) |
| anyio_mode | 字符串(str) | AnyIO 插件模式:strict|auto |
| asyncio_mode | 字符串(str) | --asyncio-mode 的默认值 |
| asyncio_debug | 布尔(bool) | 为默认事件循环启用 asyncio 调试模式(默认值) |
| asyncio_default_fixture_loop_scope | 字符串(str) | 执行异步夹具的 asyncio 事件循环默认作用域 |
| asyncio_default_test_loop_scope | 字符串(str) | 执行异步测试的 asyncio 事件循环默认作用域 |
| render_collapsed | 字符串(str) | 打开报告时默认折叠的行 |
| max_asset_filename_length | 字符串(str) | HTML 报告中附加文件的最大文件名长度 |
| environment_table_redact_list | 行列表(list) | 环境表中需要脱敏的变量正则列表(匹配的变量值会被隐藏) |
| initial_sort | 字符串(str) | 报告默认排序的列 |
| generate_report_on_test | 布尔(bool) | 每个测试执行后立即生成 HTML 报告(而非全部测试结束后) |
| reruns | 字符串(str) | --reruns 的默认值(失败测试重跑次数) |
| reruns_delay | 字符串(str) | --reruns-delay 的默认值(重跑间隔秒数) |
| rsyncdirs | 路径列表(paths) | 远程分布式测试时需要 rsync 同步的相对路径列表 |
| rsyncignore | 路径列表(paths) | rsync 同步时忽略的相对路径通配符列表 |
| looponfailroots | 路径列表(paths) | --looponfail 模式下监听文件修改的目录,默认当前目录 |
补充说明
- 配置项类型说明:
- 行列表(linelist):配置文件中每行一个值(如 markers);
- 参数列表(args):用空格 / 逗号分隔多个值;
- 路径列表(paths):文件 / 目录路径列表,支持相对 / 绝对路径;
- 配置文件优先级:pytest.toml > pytest.ini > tox.ini > setup.cfg > pyproject.toml;
- 布尔类型值在 ini 文件中用
true/false或1/0表示,在 toml 文件中用true/false; - 字符串类型若包含空格,需用引号包裹(如
addopts = "-vs --html=report.html")。
三、Pytest 进阶特性
1. 自定义标记(Mark):分类执行用例
1.1. 核心场景
实际项目中,测试用例分「冒烟用例」「回归用例」「异常用例」,需按需执行(如上线前只跑冒烟用例)。
1.2. 完整实现步骤
步骤 1:注册标记(在 pytest.ini 中)
ini
[pytest]
addopts = -vs
testpaths = ./tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 注册标记(必须!否则会报未知标记警告)
markers =
smoke: 冒烟用例(核心流程,必跑)
normal: 普通用例(功能覆盖)
exception: 异常场景用例(边界/错误输入)
步骤 2:给用例打标记
在 tests/test_login.py 中编写带标记的用例:
python
"""登录模块测试用例(带标记)"""
import pytest
# 模拟登录函数
def login(username, password):
if username == "admin" and password == "123456":
return {"code": 200, "msg": "登录成功"}
elif username == "":
return {"code": 400, "msg": "用户名不能为空"}
else:
return {"code": 401, "msg": "用户名或密码错误"}
# 冒烟用例:正确账号密码登录(核心流程)
@pytest.mark.smoke
def test_login_success():
result = login("admin", "123456")
assert result["code"] == 200, "正确账号登录失败"
assert result["msg"] == "登录成功", "登录提示语错误"
# 普通用例:错误密码登录
@pytest.mark.normal
def test_login_wrong_pwd():
result = login("admin", "654321")
assert result["code"] == 401, "错误密码登录状态码错误"
# 异常用例:空用户名登录
@pytest.mark.exception
def test_login_empty_username():
result = login("", "123456")
assert result["code"] == 400, "空用户名登录状态码错误"
# 多标记:同时属于冒烟和普通用例
@pytest.mark.smoke
@pytest.mark.normal
def test_login_remember_pwd():
# 模拟记住密码功能
assert True, "记住密码功能正常"
步骤 3:执行指定标记的用例
bash
# 只执行冒烟用例(上线前必跑)
pytest -m smoke
# 执行冒烟 或 异常用例
pytest -m "smoke or exception"
# 执行冒烟 且 普通用例(多标记用例)
pytest -m "smoke and normal"
执行结果解读(以 pytest -m smoke 为例):
cmd
collected 4 items / 2 deselected / 2 selected
tests/test_login.py::test_login_success PASSED
tests/test_login.py::test_login_remember_pwd PASSED
========================== 2 passed, 2 deselected in 0.02s ==========================
- 选中 2 个
smoke标记的用例,排除 2 个非冒烟用例; - 若未注册标记,会提示
PytestUnknownMarkWarning,但不影响执行(建议注册,规范且无警告)。
2. Fixture 固件:灵活的前后置操作
2.1. 核心场景
测试用例执行前需「初始化数据 / 连接数据库 / 启动浏览器」,执行后需「清理数据 / 关闭连接 / 关闭浏览器」,Fixture 替代传统 setup/teardown,支持不同作用域,更灵活。
2.2. 基础 Fixture(函数级)
python
"""Fixture基础示例:函数级前后置"""
import pytest
# 定义Fixture(默认scope="function",每个用例执行前/后运行)
@pytest.fixture
def init_login_data():
print("\n【前置操作】初始化登录测试数据:生成随机账号密码")
test_data = {
"username": "admin",
"password": "123456",
"expected_code": 200
}
yield test_data # 返回数据给用例,yield后是后置操作
print("\n【后置操作】清理登录测试数据:删除临时账号")
# 使用Fixture:直接将Fixture名作为参数传入用例
def test_login_with_fixture(init_login_data):
# 调用Fixture返回的测试数据
username = init_login_data["username"]
password = init_login_data["password"]
expected_code = init_login_data["expected_code"]
# 执行登录函数(复用之前的login函数)
result = login(username, password)
# 断言
assert result["code"] == expected_code, "登录失败"
运行 pytest test_login.py -v -s,结果如下:
cmd
tests/test_login.py::test_login_with_fixture
【前置操作】初始化登录测试数据:生成随机账号密码
PASSED
【后置操作】清理登录测试数据:删除临时账号
========================== 1 passed in 0.01s ==========================
- 每个用例执行前先运行
yield前的代码(前置),执行后运行yield后的代码(后置); - Fixture 可返回数据,用例直接接收,无需重复定义测试数据。
2.3. Fixture 作用域(实战常用)
| 作用域 | 说明 | 适用场景 | 示例 |
|---|---|---|---|
| function | 每个测试函数 / 方法执行一次 | 初始化临时数据(如每次登录新账号) | 上述 init_login_data |
| class | 每个测试类执行一次 | 类内所有用例共享一个数据库连接 | 下文 init_db_class |
| module | 每个测试文件执行一次 | 整个文件共享一个浏览器实例 | init_browser |
| session | 整个测试会话执行一次 | 所有用例共享一个数据库连接 | init_db_session |
类级 Fixture 示例:
python
import pytest
# 类级Fixture:每个测试类只执行一次前后置
@pytest.fixture(scope="class")
def init_db_class():
print("\n【类前置】连接数据库(类级)")
db_conn = "数据库连接对象"
yield db_conn
print("\n【类后置】关闭数据库连接(类级)")
# 测试类:使用类级Fixture
class TestUser:
# 第一个用例:执行类前置
def test_query_user(self, init_db_class):
print("查询用户信息")
assert init_db_class == "数据库连接对象"
# 第二个用例:不执行类前置(复用连接)
def test_update_user(self, init_db_class):
print("更新用户信息")
assert init_db_class == "数据库连接对象"
运行结果:
cmd
tests/test_user.py::TestUser::test_query_user
【类前置】连接数据库(类级)
查询用户信息
PASSED
tests/test_user.py::TestUser::test_update_user
更新用户信息
PASSED
【类后置】关闭数据库连接(类级)
- 类内两个用例共享 Fixture,只执行一次前置 / 后置,节省资源。
2.4.全局 Fixture(conftest.py)
核心优势:无需导入,所有测试文件自动生效,适合全局通用的前后置(如数据库连接、浏览器启动)。
步骤 1:创建 conftest.py(项目根目录)
python
"""全局Fixture文件:所有测试文件可直接使用"""
import pytest
# 会话级Fixture:整个测试过程只执行一次
@pytest.fixture(scope="session")
def init_db_session():
print("\n【全局前置】连接数据库(会话级)")
db_conn = "全局数据库连接对象"
yield db_conn
print("\n【全局后置】关闭数据库连接(会话级)")
# 函数级Fixture:所有用例可使用的通用数据
@pytest.fixture
def common_data():
return {
"base_url": "https://api.test.com",
"timeout": 10
}
步骤 2:在任意测试文件中使用(无需导入)
python
"""测试接口用例:使用全局Fixture"""
def test_query_api(init_db_session, common_data):
# 使用会话级Fixture的数据库连接
print(f"数据库连接:{init_db_session}")
# 使用通用数据Fixture
print(f"接口基础URL:{common_data['base_url']}")
assert common_data["timeout"] == 10
运行结果:
cmd
tests/test_api.py::test_query_api
【全局前置】连接数据库(会话级)
数据库连接:全局数据库连接对象
接口基础URL:https://api.test.com
PASSED
【全局后置】关闭数据库连接(会话级)
3.参数化:批量生成用例
3.1.核心场景
测试同一功能的多组数据(如登录的正确 / 错误 / 空账号场景),无需重复编写用例,批量验证。
3.2.基础参数化
python
"""参数化基础示例:登录多场景验证"""
import pytest
# 定义测试数据:(用户名, 密码, 预期状态码, 预期提示语)
test_login_data = [
("admin", "123456", 200, "登录成功"), # 场景1:正确账号
("admin", "654321", 401, "用户名或密码错误"), # 场景2:错误密码
("", "123456", 400, "用户名不能为空"), # 场景3:空用户名
("test", "123456", 401, "用户名或密码错误"), # 场景4:不存在的账号
]
# 模拟登录函数
def login(username, password):
if username == "admin" and password == "123456":
return {"code": 200, "msg": "登录成功"}
elif username == "":
return {"code": 400, "msg": "用户名不能为空"}
else:
return {"code": 401, "msg": "用户名或密码错误"}
# 参数化装饰器:(参数名, 测试数据)
@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_login_data)
def test_login_param(username, password, expected_code, expected_msg):
# 执行登录函数
result = login(username, password)
# 断言状态码和提示语
assert result["code"] == expected_code, f"场景:{username}/{password},状态码错误"
assert result["msg"] == expected_msg, f"场景:{username}/{password},提示语错误"
运行 pytest test_login.py -v -s,结果如下:
cmd
collected 4 items
tests/test_login.py::test_login_param[admin-123456-200-登录成功] PASSED
tests/test_login.py::test_login_param[admin-654321-401-用户名或密码错误] PASSED
tests/test_login.py::test_login_param[-123456-400-用户名不能为空] PASSED
tests/test_login.py::test_login_param[test-123456-401-用户名或密码错误] PASSED
========================== 4 passed in 0.03s ==========================
- 自动生成 4 个用例,每个用例对应一组数据;
- 用例名称中显示参数值,便于定位失败场景(如某组数据失败,可直接看到是哪个账号 / 密码)。
3.3.多组参数组合(笛卡尔积)
适用于 "多维度参数验证"(如不同账号 + 不同密码组合):
python
"""多组参数组合:笛卡尔积"""
import pytest
# 第一组参数:用户名列表
@pytest.mark.parametrize("username", ["admin", "test"])
# 第二组参数:密码列表
@pytest.mark.parametrize("password", ["123456", "654321"])
def test_login_combine(username, password):
print(f"\n测试组合:用户名={username},密码={password}")
result = login(username, password)
# 断言:只有admin/123456返回200,其余返回401
if username == "admin" and password == "123456":
assert result["code"] == 200
else:
assert result["code"] == 401
运行结果:
cmd
collected 4 items
tests/test_login.py::test_login_combine[123456-admin]
测试组合:用户名=admin,密码=123456
PASSED
tests/test_login.py::test_login_combine[123456-test]
测试组合:用户名=test,密码=123456
PASSED
tests/test_login.py::test_login_combine[654321-admin]
测试组合:用户名=admin,密码=654321
PASSED
tests/test_login.py::test_login_combine[654321-test]
测试组合:用户名=test,密码=654321
PASSED
- 2 个用户名 × 2 个密码 = 4 个组合,自动生成 4 个用例;
- 适合验证多维度参数的覆盖场景。
4.代码中运行 Pytest(pytest.main ())
适用于集成到自动化框架,灵活控制执行逻辑:
python
"""在代码中运行Pytest,自定义执行逻辑"""
import pytest
if __name__ == "__main__":
# 场景1:执行冒烟用例 + 生成HTML报告
pytest.main([
"-vs",
"-m", "smoke",
"--html=smoke_report.html",
"--self-contained-html"
])
# 场景2:执行指定文件 + 失败重试
# pytest.main([
# "-vs",
# "tests/test_login.py",
# "--reruns=3"
# ])
# 场景3:执行所有用例并并行
# pytest.main([
# "-vs",
# "-n", "auto"
# ])
运行该文件(如 run_test.py),效果与终端执行命令一致,适合编写自动化脚本。
5. 插件生态:扩展核心功能
5.1. 生成 HTML 可视化报告(pytest-html)
bash
# 安装插件
pip install pytest-html==3.2.0
# 运行用例并生成报告
pytest --html=report.html --self-contained-html
-
--self-contained-html:将 CSS / 图片嵌入报告,避免打开时样式丢失; -
运行完成后,项目根目录生成
report.html,打开可看到:- 用例总数、通过数、失败数、跳过数;
- 每个用例的执行时间、失败原因(带详细日志);
- 可视化统计图表(通过率、耗时分布)。
5.2. 失败重试(pytest-rerunfailures)
bash
# 安装插件
pip install pytest-rerunfailures==11.1.2
# 运行用例:失败用例重试3次,每次间隔2秒
pytest --reruns=3 --reruns-delay=2
- 适用于 "偶发失败" 的场景(如网络波动导致接口测试失败);
- 重试后成功则标记为
PASSED,仍失败则标记为FAILED。
5.3. 并行执行(pytest-xdist)
bash
# 安装插件
pip install pytest-xdist==3.5.0
# 按CPU核心数并行执行(提升执行效率)
pytest -n auto
- 适用于用例数量多的场景(如 100 个用例串行需 10 分钟,并行只需 2-3 分钟);
-n auto:自动识别 CPU 核心数,也可指定数量(如-n 4用 4 个进程)。
四、Pytest 高级特性(工程化落地)
1. YAML 数据驱动(解耦测试数据与代码)
1.1. 核心价值
实际项目中,测试数据(如登录的多组账号、接口的入参 / 预期结果)若写死在代码里,修改 / 维护成本高;YAML 数据驱动将数据与代码分离,只需修改 YAML 文件即可更新测试场景,适配大规模用例管理。
1.2. 完整实现步骤
步骤 1:安装依赖
bash
# PyYAML:解析YAML文件
pip install PyYAML==6.0.3
步骤 2:编写 YAML 测试数据文件
在项目根目录创建 data/test_login.yaml,存放登录测试数据:
yaml
# 登录模块测试数据
login_success: # 场景1:正确登录
username: "admin"
password: "123456"
expected_code: 200
expected_msg: "登录成功"
login_wrong_pwd: # 场景2:错误密码
username: "admin"
password: "654321"
expected_code: 401
expected_msg: "用户名或密码错误"
login_empty_user: # 场景3:空用户名
username: ""
password: "123456"
expected_code: 400
expected_msg: "用户名不能为空"
步骤 3:编写读取 YAML 的工具函数
创建 utils/yaml_util.py,封装读取 YAML 的通用方法:
python
"""YAML文件读取工具类"""
import yaml
import os
def read_yaml(file_path):
"""
读取YAML文件
:param file_path: YAML文件路径
:return: 解析后的字典/列表
"""
# 校验文件是否存在
if not os.path.exists(file_path):
raise FileNotFoundError(f"YAML文件不存在:{file_path}")
# 读取并解析YAML
with open(file_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return data
# 测试工具函数(可选)
if __name__ == "__main__":
test_data = read_yaml("../data/test_login.yaml")
print(test_data)
工具函数执行结果:
cmd
{
'login_success': {'username': 'admin', 'password': '123456', 'expected_code': 200, 'expected_msg': '登录成功'},
'login_wrong_pwd': {'username': 'admin', 'password': '654321', 'expected_code': 401, 'expected_msg': '用户名或密码错误'},
'login_empty_user': {'username': '', 'password': '123456', 'expected_code': 400, 'expected_msg': '用户名不能为空'}
}
步骤 4:结合 Pytest 参数化实现数据驱动
创建 tests/test_login_yaml.py:
python
"""YAML数据驱动的登录测试用例"""
import pytest
from utils.yaml_util import read_yaml
from tests.test_login import login # 复用之前的login函数
# 读取YAML测试数据
login_data = read_yaml("../data/test_login.yaml")
# 将YAML字典转换为参数化所需的列表格式
test_data_list = [
(
login_data["login_success"]["username"],
login_data["login_success"]["password"],
login_data["login_success"]["expected_code"],
login_data["login_success"]["expected_msg"]
),
(
login_data["login_wrong_pwd"]["username"],
login_data["login_wrong_pwd"]["password"],
login_data["login_wrong_pwd"]["expected_code"],
login_data["login_wrong_pwd"]["expected_msg"]
),
(
login_data["login_empty_user"]["username"],
login_data["login_empty_user"]["password"],
login_data["login_empty_user"]["expected_code"],
login_data["login_empty_user"]["expected_msg"]
)
]
# 参数化:使用YAML数据
@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_data_list)
def test_login_yaml(username, password, expected_code, expected_msg):
result = login(username, password)
# 断言
assert result["code"] == expected_code, f"场景:{username}/{password} 状态码错误"
assert result["msg"] == expected_msg, f"场景:{username}/{password} 提示语错误"
步骤 5:运行用例 & 结果解读
bash
pytest tests/test_login_yaml.py -v -s
执行结果:
cmd
collected 3 items
tests/test_login_yaml.py::test_login_yaml[admin-123456-200-登录成功] PASSED
tests/test_login_yaml.py::test_login_yaml[admin-654321-401-用户名或密码错误] PASSED
tests/test_login_yaml.py::test_login_yaml[-123456-400-用户名不能为空] PASSED
========================== 3 passed in 0.02s ==========================
实战价值 :后续新增 / 修改登录场景,只需修改 test_login.yaml,无需改动测试代码,降低维护成本。
2. 日志模块集成(可追溯的测试过程)
2.1. 核心价值
print 输出仅在终端可见,且无法分级 / 持久化;集成 Python 标准 logging 模块,可实现:
- 日志分级(DEBUG/INFO/WARNING/ERROR):调试时看详细日志,运行时看关键日志;
- 日志持久化:保存到文件,用例失败后可追溯完整执行过程;
- 日志格式化:包含时间、模块、级别、内容,便于定位问题。
2.2. 完整实现步骤
步骤 1:编写日志配置文件
创建 config/log_config.py,封装日志配置:
python
"""日志配置模块:全局通用"""
import logging
import os
from logging.handlers import RotatingFileHandler
# 日志保存路径(不存在则创建)
log_dir = "./logs"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
def setup_logger():
"""
配置日志:同时输出到终端 + 保存到文件
:return: 配置好的logger对象
"""
# 1. 创建logger对象
logger = logging.getLogger("pytest_auto_test")
logger.setLevel(logging.DEBUG) # 全局日志级别(最低级别,保证所有日志都能捕获)
logger.handlers.clear() # 避免重复添加处理器
# 2. 定义日志格式
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
# 3. 控制台处理器(输出INFO及以上级别)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# 4. 文件处理器(输出DEBUG及以上级别,按大小分割)
file_handler = RotatingFileHandler(
filename=os.path.join(log_dir, "auto_test.log"),
maxBytes=10*1024*1024, # 单个日志文件最大10MB
backupCount=5, # 最多保留5个备份文件
encoding="utf-8"
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(formatter)
# 5. 添加处理器到logger
logger.addHandler(console_handler)
logger.addHandler(file_handler)
return logger
# 全局logger对象(所有模块可直接导入使用)
logger = setup_logger()
步骤 2:在测试用例中使用日志
修改 tests/test_login_yaml.py,替换 print 为日志:
python
"""集成日志的登录测试用例"""
import pytest
from utils.yaml_util import read_yaml
from tests.test_login import login
from config.log_config import logger # 导入全局logger
# 读取YAML数据(同上,省略)
login_data = read_yaml("../data/test_login.yaml")
test_data_list = [...] # 同上,省略
@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_data_list)
def test_login_yaml(username, password, expected_code, expected_msg):
# 记录测试场景(INFO级别)
logger.info(f"开始执行登录测试场景:用户名={username},密码={password}")
try:
# 执行登录函数
result = login(username, password)
logger.debug(f"登录接口返回结果:{result}") # DEBUG级别:详细返回值
# 断言
assert result["code"] == expected_code
assert result["msg"] == expected_msg
logger.info(f"登录测试场景通过:用户名={username}")
except AssertionError as e:
# 记录失败日志(ERROR级别)
logger.error(f"登录测试场景失败:{e}")
raise # 重新抛出异常,保证pytest捕获失败
步骤 3:运行用例 & 日志效果
bash
pytest tests/test_login_yaml.py -v
终端日志输出(INFO 级别):
cmd
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:20 - 开始执行登录测试场景:用户名=admin,密码=123456
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:28 - 登录测试场景通过:用户名=admin
日志文件(logs/auto_test.log)(包含 DEBUG 级别):
cmd
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:20 - 开始执行登录测试场景:用户名=admin,密码=123456
2026-03-13 15:30:00 - pytest_auto_test - DEBUG - test_login_yaml.py:25 - 登录接口返回结果:{'code': 200, 'msg': '登录成功'}
2026-03-13 15:30:00 - pytest_auto_test - INFO - test_login_yaml.py:28 - 登录测试场景通过:用户名=admin
实战价值:用例失败时,可通过日志文件查看完整执行过程(如接口返回值、参数是否正确),快速定位问题,而非仅依赖终端的断言错误。
3. 配置文件管理(多环境适配)
3.1. 核心价值
实际项目中存在「测试环境、预发环境、生产环境」,接口地址、数据库配置等需根据环境切换;将配置抽离为独立文件(如 config.yaml),通过参数控制环境,避免硬编码,适配多环境测试。
3.2. 完整实现步骤
步骤 1:编写多环境配置文件
创建 config/config.yaml:
yaml
# 多环境配置
dev: # 开发环境
base_url: "https://dev-api.test.com"
db:
host: "127.0.0.1"
port: 3306
user: "dev_user"
password: "dev_pwd"
test: # 测试环境(默认)
base_url: "https://test-api.test.com"
db:
host: "192.168.1.100"
port: 3306
user: "test_user"
password: "test_pwd"
prod: # 生产环境(谨慎使用)
base_url: "https://api.test.com"
db:
host: "10.0.0.1"
port: 3306
user: "prod_user"
password: "prod_pwd"
步骤 2:封装配置读取工具
修改 utils/yaml_util.py,新增读取配置的方法:
python
"""YAML文件读取工具类(新增配置读取)"""
import yaml
import os
def read_yaml(file_path):
"""读取普通YAML测试数据(原有方法)"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"YAML文件不存在:{file_path}")
with open(file_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return data
def read_config(env="test"):
"""
读取多环境配置
:param env: 环境名(dev/test/prod),默认test
:return: 指定环境的配置字典
"""
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config/config.yaml")
config_data = read_yaml(config_path)
if env not in config_data:
raise ValueError(f"不支持的环境:{env},可选环境:dev/test/prod")
return config_data[env]
# 测试配置读取(可选)
if __name__ == "__main__":
test_config = read_config("test")
print(f"测试环境接口地址:{test_config['base_url']}")
dev_config = read_config("dev")
print(f"开发环境数据库地址:{dev_config['db']['host']}")
工具函数执行结果:
cmd
测试环境接口地址:https://test-api.test.com
开发环境数据库地址:127.0.0.1
步骤 3:在测试用例中使用多环境配置
修改 tests/test_login_yaml.py,适配多环境接口地址:
python
"""多环境配置的登录测试用例"""
import pytest
import requests # 模拟真实接口请求
from utils.yaml_util import read_yaml, read_config
from config.log_config import logger
# 读取YAML测试数据 + 环境配置
login_data = read_yaml("../data/test_login.yaml")
env_config = read_config(env="test") # 可通过命令行参数动态指定
base_url = env_config["base_url"]
# 转换测试数据(原有逻辑,省略)
test_data_list = [...]
@pytest.mark.parametrize("username, password, expected_code, expected_msg", test_data_list)
def test_login_yaml(username, password, expected_code, expected_msg):
logger.info(f"【{env_config}环境】开始执行登录测试:{username}/{password}")
# 真实接口请求(结合环境配置的base_url)
url = f"{base_url}/login"
payload = {"username": username, "password": password}
try:
response = requests.post(url, json=payload, timeout=10)
result = response.json()
logger.debug(f"接口返回:{result}")
# 断言
assert result["code"] == expected_code
assert result["msg"] == expected_msg
logger.info(f"【{env_config}环境】登录测试通过:{username}")
except Exception as e:
logger.error(f"【{env_config}环境】登录测试失败:{e}")
raise
步骤 4:动态指定环境运行用例
修改 run_test.py,支持通过命令行参数指定环境:
python
"""支持多环境的测试执行入口"""
import pytest
import os
import sys
from config.log_config import logger
from utils.yaml_util import read_config
if __name__ == "__main__":
# 1. 获取命令行传入的环境参数(默认test)
env = sys.argv[1] if len(sys.argv) > 1 else "test"
try:
read_config(env) # 校验环境是否合法
except ValueError as e:
logger.error(e)
sys.exit(1)
# 2. 记录环境信息
logger.info(f"========== 开始执行【{env}】环境自动化测试 ==========")
# 3. 执行参数(原有逻辑,新增环境标记)
report_path = f"./reports/{env}_test_report.html"
pytest_args = [
"-vs",
"-m", "smoke",
f"--html={report_path}",
"--self-contained-html",
"--reruns=2",
"-n", "auto"
]
# 4. 执行测试(原有逻辑,省略)
pytest.main(pytest_args)
步骤 5:运行用例 & 结果解读
bash
# 执行测试环境用例
python run_test.py test
# 执行开发环境用例
python run_test.py dev
执行结果:
cmd
2026-03-13 16:00:00 - pytest_auto_test - INFO - run_test.py:15 - ========== 开始执行【test】环境自动化测试 ==========
2026-03-13 16:00:00 - pytest_auto_test - INFO - test_login_yaml.py:20 - 【test环境】开始执行登录测试:admin/123456
2026-03-13 16:00:01 - pytest_auto_test - DEBUG - test_login_yaml.py:28 - 接口返回:{'code': 200, 'msg': '登录成功'}
2026-03-13 16:00:01 - pytest_auto_test - INFO - test_login_yaml.py:32 - 【test环境】登录测试通过:admin
实战价值:无需修改代码,仅通过命令行参数即可切换测试环境,适配不同阶段的测试需求(如开发环境联调、测试环境回归)。
4. 失败用例重跑 & 精准重试(精细化控制)
4.1. 核心价值
pytest-rerunfailures 插件默认对所有失败用例重试,但实际场景中需「只重试网络类失败」「按标记重试」「限制重试间隔」,精细化控制重试逻辑,避免无效重试。
4.2. 完整实现步骤
步骤 1:安装插件(原有)
bash
pip install pytest-rerunfailures==11.1.2
步骤 2:按标记 / 异常类型重试
修改 pytest.ini,配置精细化重试规则:
ini
[pytest]
addopts = -vs --reruns 2 --reruns-delay 3 # 全局重试2次,间隔3秒
testpaths = ./tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
smoke: 冒烟用例
normal: 普通用例
retry: 需要重试的用例(如接口类)
步骤 3:给指定用例标记重试
python
"""精细化重试的接口测试用例"""
import pytest
import requests
from config.log_config import logger
from utils.yaml_util import read_config
env_config = read_config("test")
base_url = env_config["base_url"]
# 只给接口用例标记retry,且只重试ConnectionError异常
@pytest.mark.retry
def test_query_user():
url = f"{base_url}/user/info"
try:
response = requests.get(url, timeout=5)
assert response.status_code == 200
except requests.exceptions.ConnectionError as e:
logger.warning(f"网络连接失败,触发重试:{e}")
raise # 抛出异常触发重试
except Exception as e:
logger.error(f"非网络异常,不重试:{e}")
raise # 不重试,直接失败
# 普通用例:不重试
def test_calculate():
assert 1 + 2 == 4 # 断言失败,直接失败,不重试
步骤 4:运行用例 & 结果解读
bash
pytest tests/test_retry.py -v --reruns 2 --reruns-delay 3 -m retry
执行结果(模拟网络失败):
cmd
tests/test_retry.py::test_query_user RERUN # 第一次失败,重试
tests/test_retry.py::test_query_user RERUN # 第二次失败,重试
tests/test_retry.py::test_query_user FAILED # 最终失败
========================== 1 failed in 9.01s ==========================
关键说明:
--reruns 2:最多重试 2 次,总执行次数 = 1(原执行)+2(重试)=3 次;--reruns-delay 3:每次重试间隔 3 秒,避免高频请求触发接口限流;-m retry:只对标记为retry的用例重试,普通用例不重试。
实战价值:针对网络波动、接口限流等偶发失败场景精准重试,减少误报;普通用例(如计算、逻辑判断)失败不重试,提升测试效率。
5. 测试报告增强(Allure 报告)
Allure 是一款开源的测试报告生成框架,专为测试结果的可视化、可交互性设计,常与 Pytest、JUnit、TestNG 等测试框架集成,是 Python 自动化测试中主流的报告工具之一。
5.1. 核心价值
pytest-html 报告轻量化但功能简单,Allure 报告支持「用例分层、步骤展示、图表统计、失败截图 / 日志关联」,是企业级自动化测试的首选报告工具。
5.2. 完整实现步骤
步骤 1:安装Allure
✅ 下载链接:https://github.com/allure-framework/allure2/releases(https://github.com/allure-framework/allure2/releases/download/2.30.0/allure 2.30.0.zip)
✅ 添加至环境变量:

✅ 验证安装是否成功:
cmd
allure --version
出现 allure 版本则安装成功
步骤 2:安装客户端依赖
bash
# 安装Allure pytest插件
pip install allure-pytest
# 下载Allure命令行工具(需配置环境变量)
# Windows:https://github.com/allure-framework/allure2/releases
# Mac:brew install allure
# Linux:sudo apt-get install allure
# 若已安装,可升级到最新版本
pip install --upgrade allure-pytest
步骤 3:编写带 Allure 步骤的用例
python
"""Allure增强报告的测试用例"""
import pytest
import allure
from utils.yaml_util import read_yaml
from config.log_config import logger
# 读取测试数据
login_data = read_yaml("../data/test_login.yaml")
@allure.epic("用户模块") # 大模块(如项目级)
@allure.feature("登录功能") # 功能模块
@allure.story("正确账号密码登录") # 具体场景
@pytest.mark.smoke
def test_login_success():
# 拆分测试步骤(Allure报告中可展开查看)
with allure.step("步骤1:准备测试数据"):
username = login_data["login_success"]["username"]
password = login_data["login_success"]["password"]
logger.info(f"测试数据:{username}/{password}")
with allure.step("步骤2:执行登录操作"):
result = login(username, password) # 复用login函数
with allure.step("步骤3:断言结果"):
assert result["code"] == 200
assert result["msg"] == "登录成功"
# 附加测试数据到报告
allure.attach(str(login_data["login_success"]), "测试数据", allure.attachment_type.JSON)
步骤 4:生成 Allure 报告
bash
# 步骤1:生成Allure原始数据
pytest tests/test_allure.py -v --alluredir=./reports/allure-results
# 步骤2:生成HTML报告(本地预览)
allure serve ./reports/allure-results
# 步骤3:生成静态HTML报告(便于分享)
allure generate ./reports/allure-results -o ./reports/allure-report --clean --encoding utf-8
步骤 5:Allure 报告效果

- 左侧导航:按
epic/feature/story分层展示用例,可快速筛选; - 用例详情:显示每个
allure.step的执行结果,失败步骤标红; - 附件:可查看附加的测试数据、日志、截图;
- 统计图表:展示用例通过率、执行时长、失败分布。
实战价值:Allure 报告结构化展示测试结果,便于团队协作(如开发查看失败步骤、测试负责人统计通过率),是企业级自动化测试的标准配置。
6. 自动化测试实战(完整流程封装)
3.1. 核心场景
结合「Fixture + YAML 数据驱动 + 日志 + 报告」,搭建一套可落地的接口自动化测试框架,覆盖 "前置准备→用例执行→结果记录→报告生成" 全流程。
3.2. 框架目录结构(规范且易维护)
pytest_auto_test/
├── config/ # 配置目录
│ └── log_config.py # 日志配置
├── data/ # 测试数据目录
│ └── test_login.yaml # 登录测试数据
├── logs/ # 日志输出目录
├── reports/ # 测试报告目录
├── tests/ # 测试用例目录
│ ├── test_login.py # 登录功能基础用例
│ └── test_login_yaml.py # YAML数据驱动用例
├── utils/ # 工具类目录
│ └── yaml_util.py # YAML读取工具
├── conftest.py # 全局Fixture
├── pytest.ini # pytest全局配置
└── run_test.py # 测试执行入口
3.3. 测试执行入口(run_test.py)
python
"""自动化测试执行入口:一键运行所有用例"""
import pytest
import os
from config.log_config import logger
if __name__ == "__main__":
# 1. 记录开始日志
logger.info("========== 自动化测试开始执行 ==========")
# 2. 定义执行参数
report_path = "./reports/test_report.html"
pytest_args = [
"-vs", # 详细输出 + 显示日志
"-m", "smoke", # 只执行冒烟用例
"--html=" + report_path, # 生成HTML报告
"--self-contained-html", # 报告样式内嵌
"--reruns=2", # 失败重试2次
"-n", "auto" # 并行执行
]
# 3. 执行测试
try:
pytest.main(pytest_args)
logger.info("========== 自动化测试执行完成 ==========")
except Exception as e:
logger.error(f"自动化测试执行异常:{e}")
raise
# 4. 验证报告生成
if os.path.exists(report_path):
logger.info(f"测试报告已生成:{os.path.abspath(report_path)}")
else:
logger.warning("测试报告生成失败!")
3.4. 运行入口文件 & 完整流程
bash
python run_test.py
执行流程:
- 初始化全局日志 →
- 加载
conftest.py中的全局 Fixture(如数据库连接)→ - 读取 YAML 测试数据 →
- 并行执行冒烟用例 →
- 失败用例自动重试 →
- 生成 HTML 报告 →
- 记录执行日志 →
- 输出报告路径。
最终效果:
- 日志文件:记录所有执行步骤和用例结果;
- HTML 报告:可视化展示用例通过率、失败原因、执行时间;
- 一键执行:无需手动输入复杂命令,适合非技术人员 / 定时任务执行。