2、pytest快速上手:pytest安装与配置、断言机制、测试用例编写规范、运行模式
好,咱们直接进入正题。pytest 这东西,说白了就是 Python 测试界的瑞士军刀。我最早用 unittest 的时候,写个测试用例得继承 TestCase,还得记一堆 assertEqual、assertTrue 的方法名,烦得很。后来换成 pytest,第一感觉就是——这才叫「写代码」嘛。
2.1 安装与配置
安装没什么好说的,一行命令搞定:
pip install pytest
我个人习惯在项目里创建一个 requirements-dev.txt,把 pytest 和它的插件都写进去。这样团队成员拉下来直接装,省得扯皮。
验证安装是否成功,跑一下:
pytest --version
嗯,看到版本号就说明成了。
配置方面,pytest 支持 pytest.ini、pyproject.toml、tox.ini 这些文件。我一般用 pytest.ini,简单直接:
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --tb=short
这里我解释一下几个关键配置:
- testpaths:告诉 pytest 去哪找测试文件。默认是当前目录,但我习惯把测试都扔到
tests/文件夹里。 - python_files:匹配测试文件的命名规则。默认就是
test_*.py,你也可以改成*_test.py,看团队习惯。 - addopts:每次运行都自动加上的参数。
-v是详细输出,--tb=short是缩短错误堆栈。我刚开始用 pytest 时,错误堆栈能刷一屏幕,看得眼花。后来加了--tb=short,清爽多了。
filterwarnings = ignore::DeprecationWarning,能省不少心。
2.2 断言机制
pytest 的断言,说白了就是 Python 原生的 assert 关键字。你想想看,不用记那些乱七八糟的 assertEqual、assertTrue,直接写 assert a == b 就行了。多舒服。
来看几个例子:
def test_addition():
result = 1 + 1
assert result == 2
def test_string():
name = "pytest"
assert "py" in name
def test_list():
items = [1, 2, 3]
assert len(items) == 3
assert 2 in items
def test_float():
value = 0.1 + 0.2
assert abs(value - 0.3) < 1e-6 # 浮点数比较要小心
这里有个坑,我必须要说一下。浮点数比较,千万别直接 assert 0.1 + 0.2 == 0.3。为什么?因为计算机里浮点数有精度问题。0.1 + 0.2 实际上等于 0.30000000000000004。我曾经在一个财务系统里吃过这个亏,算出来的金额差了一分钱,查了半天才发现是浮点数比较的问题。
pytest 也提供了更高级的断言用法,比如异常断言:
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
result = 1 / 0
这个 pytest.raises 特别好用。你想想看,如果你要测试一个函数在输入非法参数时会不会抛出异常,用这个就对了。
^ 符号标出哪里不匹配。这个特性在调试时特别有用。
2.3 测试用例编写规范
写测试用例,说白了就是写给人看的。我见过不少团队,测试代码写得比生产代码还乱,最后没人愿意维护。所以,规范很重要。
pytest 的测试用例编写,遵循几个简单的规则:
- 文件名:以
test_开头,比如test_user.py - 类名:以
Test开头,比如TestUserAPI - 函数名:以
test_开头,比如test_create_user
来看一个完整的例子:
# tests/test_user.py
class TestUserAPI:
def test_create_user(self):
"""测试创建用户接口"""
# 准备数据
user_data = {"name": "张三", "age": 25}
# 执行操作
result = create_user(user_data)
# 验证结果
assert result["status"] == "success"
assert result["user_id"] is not None
def test_create_user_with_empty_name(self):
"""测试创建用户时姓名为空"""
user_data = {"name": "", "age": 25}
with pytest.raises(ValueError, match="姓名不能为空"):
create_user(user_data)
我个人习惯在每个测试函数上加一个 docstring,说明这个测试是测什么的。为什么?因为半年后你回来看代码,可能完全想不起来这个 test_create_user 到底在测什么。docstring 就是给你自己看的备忘录。
还有一个规范,叫「三A原则」:
- Arrange(准备):准备好测试数据和环境
- Act(执行):执行被测试的代码
- Assert(断言):验证结果是否符合预期
你看上面的例子,我就是按这个顺序写的。代码里用空行隔开,一目了然。
2.4 运行模式
pytest 的运行模式非常灵活。我平时最常用的几种方式:
| 命令 | 说明 |
|---|---|
pytest |
运行所有测试 |
pytest tests/ |
运行指定目录下的测试 |
pytest test_user.py |
运行指定文件 |
pytest test_user.py::TestUserAPI |
运行指定测试类 |
pytest test_user.py::TestUserAPI::test_create_user |
运行指定测试函数 |
pytest -k "create" |
按名称匹配运行(包含"create"的测试) |
pytest -m "slow" |
按标记运行(需要先给测试加 @pytest.mark.slow) |
这里我重点说一下 -k 参数。它支持表达式匹配,比如:
pytest -k "create or update" # 运行名称包含 create 或 update 的测试
pytest -k "not delete" # 运行名称不包含 delete 的测试
这个功能在调试时特别有用。比如你改了一个函数,想看看相关的测试有没有全过,直接用 -k 匹配函数名就行了。
还有一个我经常用的参数是 -x,遇到第一个失败就停止:
pytest -x
为什么用这个?因为有时候测试跑得慢,你不想等全部跑完才发现第一个就挂了。先修好第一个,再跑剩下的。
最后,说说 --pdb 参数。这个参数会在测试失败时自动进入调试模式:
pytest --pdb
嗯,这个我强烈推荐。遇到复杂的失败场景,直接进调试器看变量值,比看错误堆栈快多了。
pytest -v --tb=short,快速定位问题。提交代码前用 pytest -v --tb=long 跑一遍完整测试,确保没有遗漏。
好了,pytest 的基础就这些。下一章我们会深入 fixture 和参数化,那才是 pytest 真正强大的地方。先把这个基础打牢,后面学起来就顺了。