pytest 入门与实践:从第一个测试到可维护的测试体系
面向 Python 开发者与测试工程师的实战指南,涵盖安装、发现规则、断言、fixture、参数化、标记、配置、常用命令和团队实践。
阅读对象:刚开始写 Python 自动化测试的开发者、测试工程师,以及希望改进测试项目的团队。
预计阅读:约 5 分钟 ·示例环境:Python 3.10+、pytest
https://github.com/lfl171/pytest_zhishi.git
目录
- pytest 是什么
- 安装与第一个测试
- 测试发现与命名
- assert 与失败信息
- Fixture:组织前置条件和清理
- 参数化
- Marker:给测试分类与筛选
- 常用内置能力
- 配置与命令行
- 推荐项目结构与实践
- 常见问题
- 小结
1. pytest 是什么
pytest 是 Python 生态中广泛使用的测试框架。它适用于从小型单元测试到集成测试的多种场景,也能运行许多基于unittest的现有测试。主要特点如下:
- 低门槛:普通函数配合 Python 原生
assert即可编写测试,无需继承特定测试基类。 - 清晰的失败诊断:断言失败时展示表达式中的实际值,便于定位问题。
- Fixture 机制:复用依赖与资源准备,并管理生命周期和清理。
- 参数化与标记:扩展输入组合,并按类别选择测试。
- 插件生态:接入覆盖率、并行执行、浏览器自动化等能力。
pytest 是测试运行与组织工具,不会自动判断产品需求是否覆盖充分。测试质量仍取决于清晰的预期、有效的边界分析和可靠的测试数据。
2. 安装与第一个测试
推荐在项目虚拟环境中安装,避免依赖污染系统 Python:
python-mvenv .venv# Windows PowerShell.venv\Scripts\Activate.ps1# macOS / Linux: source .venv/bin/activatepython-mpipinstallpytest创建test_calculator.py:
defadd(a:int,b:int)->int:returna+bdeftest_add_two_positive_numbers():assertadd(2,3)==5在项目根目录运行:
python-mpytest成功时通常会看到1 passed。使用python -m pytest可明确通过当前 Python 环境执行模块;也可以直接运行pytest。
3. 测试发现与命名
默认情况下,pytest 会递归发现符合约定的测试文件、类和函数:
- 文件名通常是
test_*.py或*_test.py。 - 函数名以
test_开头。 - 测试类以
Test开头,且通常不定义__init__。 - 类中的测试方法以
test_开头。
明确且一致的命名让收集行为可预测。可用pytest --collect-only先检查将运行哪些测试;只运行某个文件、类或函数时,可用节点路径:
pytest --collect-only pytest tests/test_user.py pytest tests/test_user.py::TestUser::test_create_user测试函数名宜描述行为和场景,例如test_rejects_expired_token,而不是test_case_03。若项目采用src/布局,建议将产品代码作为已安装包导入,并在项目配置中设置测试路径,减少因当前工作目录不同导致的导入差异。
4. assert 与失败信息
直接使用 Python 的assert表达预期:
deftest_discounted_price():price=100discount=0.2final_price=price*(1-discount)assertfinal_price==80assertfinal_price<price失败时 pytest 会重写断言表达式,展示参与比较的值。测试应验证对使用者有意义的行为,而非重复实现被测函数的内部算法。对浮点数,优先使用容差比较:
assertactual==pytest.approx(0.3)5. Fixture:组织前置条件和清理
Fixture 是 pytest 的依赖准备机制。测试函数把 fixture 名称写作参数,pytest 会找到对应 fixture 并注入其返回值。Fixture 可以依赖其他 fixture,形成可组合的准备流程。
importpytest@pytest.fixturedefuser_record():return{"id":7,"name":"Lin","active":True}deftest_user_is_active(user_record):assertuser_record["active"]isTrueFixture 的作用范围
scope控制同一 fixture 实例复用的生命周期:
| scope | 生命周期 | 常见用途 |
|---|---|---|
function(默认) | 每个测试函数 | 隔离性强的临时数据 |
class | 一个测试类 | 类内共享且安全的状态 |
module | 一个测试模块 | 模块级昂贵准备 |
package | 一个包 | 包内共享资源 |
session | 整次 pytest 会话 | 只读客户端、公共静态资源 |
作用范围越大,创建次数越少,但共享状态和测试耦合的风险越高。只有资源允许安全共享时才扩大范围。
使用 yield 清理资源
yield前准备资源,yield后释放。即使测试断言失败,pytest 也会执行已到达的清理部分:
@pytest.fixturedeftemp_connection():connection=open_connection()yieldconnection connection.close()对于多步资源初始化,应让每一步初始化后就有对应清理保障,避免后续步骤失败时留下半初始化资源。Fixture 应保持职责清晰;避免构造执行大量隐式动作的“万能 fixture”。
在conftest.py中定义的 fixture 可被其目录及子目录中的测试发现,不需要显式导入。建议把共享 fixture 放在合理的目录层级,避免顶层conftest.py成为难以理解的全局依赖集合。
6. 参数化:用一份测试覆盖多组数据
当测试逻辑一致、输入不同时,可用@pytest.mark.parametrize:
importpytest@pytest.mark.parametrize("text, expected",[("hello",5),("",0),("你好",2),],)deftest_character_count(text,expected):assertlen(text)==expected每组参数会作为独立用例报告,失败时能直接看到是哪组数据出错。对边界、非法输入、等价类尤其有用。给参数命名,避免把所有组合塞进一个测试函数;组合数量过多时,按风险优先级取舍,防止套件膨胀。也可参数化 fixture,或者为单条参数指定pytest.param(..., marks=...)。
7. Marker:给测试分类与筛选
内置 marker 可表达常见执行条件:
importpytest@pytest.mark.skip(reason="功能尚未支持")deftest_future_feature():...@pytest.mark.skipif(notis_windows(),reason="仅 Windows 支持")deftest_windows_behavior():...@pytest.mark.xfail(reason="上游缺陷尚未修复")deftest_known_bug():...项目也可以定义自有标记(例如slow、integration),然后按标记筛选:pytest -m "not slow"。自定义标记应在配置文件中注册,便于团队理解其意图并避免拼写错误。
标记是分类和选择机制,不应把长期失败测试无限期xfail或skip掩盖起来。标注原因,定期清理过期标记。
8. 常用内置能力
预期异常
用pytest.raises验证异常类型;必要时进一步检查异常消息:
deftest_invalid_age_is_rejected():withpytest.raises(ValueError,match="age must be positive"):create_profile(age=0)将操作放在with内部的最小范围,避免其它代码意外触发同一个异常,造成误通过。
临时文件与目录
tmp_pathfixture 为每个测试提供独立临时目录:
deftest_export_writes_json(tmp_path):output=tmp_path/"report.json"export_report(output)assertoutput.exists()assert'"status": "ok"'inoutput.read_text(encoding="utf-8")测试不必在仓库中创建、维护和清理临时文件。
Monkeypatch
内置monkeypatchfixture 可在测试期间临时替换属性、字典项、环境变量或工作目录,并在测试结束后恢复:
deftest_reads_api_key_from_environment(monkeypatch):monkeypatch.setenv("API_KEY","test-key")assertload_api_key()=="test-key"优先替换系统边界(时间、网络、环境变量、文件系统入口),避免过度模拟内部细节。
9. 配置与命令行
pytest 支持pyproject.toml、pytest.ini、tox.ini等配置形式。新项目可把相关设置集中在pyproject.toml:
[tool.pytest.ini_options] testpaths = ["tests"] addopts = "-ra" markers = [ "slow: 运行时间较长的测试", "integration: 需要外部服务或多个组件的测试", ]常用命令:
| 命令 | 用途 |
|---|---|
pytest | 运行发现到的测试 |
pytest -q | 简洁输出 |
pytest -v | 显示每个用例名称 |
pytest -x | 第一次失败后停止 |
pytest --maxfail=2 | 最多遇到指定失败数后停止 |
pytest -k "user and not slow" | 按名称表达式筛选 |
pytest -m integration | 按 marker 筛选 |
pytest --collect-only | 只收集,不执行 |
pytest --durations=10 | 显示最慢的若干用例 |
pytest --tb=short | 使用较短的回溯信息 |
参数可以组合,例如pytest -q -m "not slow"。CI 中建议保留清晰的失败信息;-x适合快速反馈,不一定适合作为唯一的完整回归运行方式。
10. 推荐项目结构与实践
my_project/ ├── pyproject.toml ├── src/ │ └── my_project/ │ └── calculator.py └── tests/ ├── conftest.py ├── unit/ │ └── test_calculator.py └── integration/ └── test_api.py实用原则:
- 先测可观察行为:输入、输出、状态变化和错误处理比私有实现细节稳定。
- 每个测试可独立运行:避免依赖执行顺序或共享可变数据。
- 失败要可诊断:测试名描述场景,断言定位明确,测试数据具有代表性。
- 管理外部依赖:将网络、数据库、时间等边界隔离;集成测试使用可控环境。
- 合理分层:快速单元测试、较慢集成测试分开组织,并按需要选择运行集合。
- 把运行方式自动化:在持续集成中安装项目依赖并运行测试,失败时保留日志。
- 插件按需添加:插件能扩展能力,也会带来依赖和维护成本;选择有明确价值的插件。
11. 常见问题
为什么没有发现测试?
检查文件名、函数名是否符合约定,确认运行目录和配置中的testpaths,然后执行pytest --collect-only -q查看收集结果。
为什么本地能导入,CI 却报模块找不到?
常见原因是本地工作目录或PYTHONPATH偶然提供了导入路径。将项目按规范安装到虚拟环境,统一 CI 工作目录和安装步骤,并检查src/布局及配置。
为什么测试之间互相影响?
通常是共享可变状态、数据库记录未清理、临时文件重名、环境变量未恢复,或测试依赖运行顺序。使用 function-scope fixture、独立数据标识和可靠清理来修复根因。
单元测试还是端到端测试?
两者解决的问题不同。单元测试反馈快、定位明确;集成或端到端测试验证更多真实组件的协作,但运行成本和环境复杂度更高。应按风险分层组合,而不是期待一种测试覆盖全部风险。
12. 小结
pytest 从简单的assert起步,借助 Fixture 复用准备逻辑,使用参数化覆盖输入空间,再用标记和配置组织不同运行集合。框架提供结构与反馈,可靠的测试仍来自独立、明确、贴近用户行为的验证。
官方资料
- pytest 官方文档:Get Started
- pytest 官方文档:Fixtures
- pytest 官方文档:参数化
- pytest 官方文档:命令行用法
- pytest 官方文档:配置选项