测试代码这件事,很多人知道重要,但真正动手写的时候往往不知道从哪里开始。pytest 就是那把能让你"开口说话"的钥匙------它足够简单,让你五分钟内跑起第一个测试;又足够强大,支撑得起大型生产系统的复杂测试需求。这篇教程会带你把核心概念摸透,同时给出贴近真实工程的代码示例。
一、pytest 是什么,为什么选它
Python 生态里的测试框架不止一个,但 pytest 已经成为事实上的行业标准。原因很朴素:
- 写法极简 ,用最普通的
assert语句就能断言,不需要记一堆self.assertEqual之类的 API - 自动发现测试,只要文件名和函数名符合约定,pytest 自己就能找到并执行
- 插件生态丰富,超过 1300 个外部插件,覆盖覆盖率统计、并行执行、异步测试等各种场景
- 与 CI/CD 无缝集成,GitHub Actions、Jenkins、GitLab CI 都能直接用
安装只需一行:
pip install pytest
写一个最简单的测试,新建文件 test_sample.py:
python
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5 # 故意写错,看看报错长什么样
执行 pytest,你会看到 pytest 给出非常清晰的失败报告:
ini
FAILED test_sample.py::test_answer - assert 4 == 5
+ where 4 = inc(3)
它直接告诉你"期望 5,实际得到 4,来自 inc(3)"------这种断言内省能力是 pytest 的一大亮点。
二、测试发现规则与项目结构
pytest 找测试文件有一套约定,记住这两条就够了:
- 文件名必须是
test_*.py或*_test.py - 测试函数必须以
test_开头
一个典型的工程项目结构长这样:
css
my_project/
├── src/
│ └── app/
│ ├── users.py
│ └── orders.py
└── tests/
├── conftest.py ← 共享配置和 fixture
├── test_users.py
└── test_orders.py
源码和测试分开放,是工程实践中的基本素养。conftest.py 是个特殊文件,后面会重点讲。
三、AAA 模式:让测试代码像文章一样好读
好的测试代码有结构感。业界流行一种叫 Arrange--Act--Assert(AAA) 的写法:
ini
def test_login_success():
# Arrange --- 准备数据
user = User("john", "123")
# Act --- 执行被测逻辑
result = authenticate(user.username, user.password)
# Assert --- 验证结果
assert result is True
三段式结构让每个测试的意图一目了然:我准备了什么、做了什么、期望什么。这不是强制要求,但养成这个习惯,你的测试代码会让同事刮目相看。
四、Fixture:测试的"后勤保障部队"
Fixture 是 pytest 最核心、也最有魅力的概念。简单说,它就是可复用的测试前置准备------数据库连接、测试用户、配置对象,这些东西你不想在每个测试里重复写,就用 fixture 封装起来。
基础用法
python
import pytest
@pytest.fixture
def sample_user():
return {"name": "Alice", "role": "admin"}
def test_user_role(sample_user): # 把 fixture 名字作为参数传入
assert sample_user["role"] == "admin"
pytest 看到函数参数名 sample_user,会自动找到同名 fixture 并注入。不需要手动调用,这就是依赖注入的魔法。
Fixture 的生命周期(scope)
不是所有资源都需要每次测试都重建。pytest 提供四种 scope:
| scope | 创建时机 | 适用场景 |
|---|---|---|
function(默认) |
每个测试函数 | 普通数据对象 |
class |
每个测试类 | 类内共享状态 |
module |
每个测试文件 | 模块级数据库连接 |
session |
整个测试会话 | 全局配置、重量级资源 |
ini
@pytest.fixture(scope="module")
def db_connection():
conn = create_fake_db()
return conn
用 yield 实现自动清理
测试完要"收拾残局",yield 让 setup 和 teardown 写在一起,逻辑清晰:
ruby
@pytest.fixture
def db_conn():
conn = create_db_connection()
yield conn # ← 测试在这里运行
conn.close() # ← 测试结束后自动执行清理
这比 setUp/tearDown 的写法优雅太多了。
五、conftest.py:跨文件共享 fixture 的秘密武器
conftest.py 是 pytest 的"公共仓库"。放在这个文件里的 fixture,同目录及子目录下的所有测试文件都能直接使用,不需要 import。
python
# tests/conftest.py
import pytest
@pytest.fixture
def auth_token():
return "Bearer test-token-xyz"
ini
# tests/test_api.py
def test_get_user(auth_token): # 直接用,不用 import
headers = {"Authorization": auth_token}
# ...
大型项目里,conftest.py 可以分层放置------根目录一个,子目录各一个,pytest 会按就近原则查找。
六、参数化测试:一个函数,跑 N 种情况
写测试最怕的就是复制粘贴。参数化(parametrize)让你用一个测试函数覆盖多组输入:
python
import pytest
@pytest.mark.parametrize(
"a, b, expected",
[
(1, 2, 3),
(5, 5, 10),
(-1, 1, 0),
(0, 0, 0),
]
)
def test_add(a, b, expected):
assert add(a, b) == expected
pytest 会把这个测试展开成 4 个独立用例,分别报告通过或失败。边界值、正常值、异常值一网打尽,覆盖率蹭蹭往上涨。
七、Mock 与 Monkeypatch:隔离外部依赖
真实项目里,代码往往要调用数据库、发 HTTP 请求、读写文件。测试时不可能真的去连数据库,这时候就需要"替身演员"------Mock。
用 unittest.mock 打桩
python
from unittest.mock import patch
@patch("app.email.send_email")
def test_send_notification(mock_send):
mock_send.return_value = True
result = send_notification("user@example.com")
assert result is True
mock_send.assert_called_once() # 验证确实被调用了
pytest 内置的 monkeypatch
monkeypatch 更轻量,特别适合替换环境变量、函数或对象属性:
python
def test_debug_mode(monkeypatch):
monkeypatch.setenv("DEBUG", "true")
assert os.environ["DEBUG"] == "true"
# 测试结束后,环境变量自动恢复原样
Mock、Stub、Spy 的区别
这三个概念经常被混淆,一张表说清楚:
| 概念 | 作用 | 典型场景 |
|---|---|---|
| Mock | 完全替换依赖对象 | 无法运行真实依赖时 |
| Stub | 只提供预设返回值 | 只关心输出,不关心行为 |
| Spy | 包裹真实逻辑并记录调用 | 验证函数是否被调用 |
八、Markers:给测试贴标签,按需执行
随着测试越来越多,你会希望能分组运行------只跑快速的单元测试,或者只跑集成测试。Markers 就是这个用途:
less
@pytest.mark.slow
def test_heavy_computation():
...
@pytest.mark.integration
def test_database_write():
...
@pytest.mark.skip(reason="功能尚未实现")
def test_future_feature():
...
# 条件跳过
@pytest.mark.skipif(sys.platform == "win32", reason="不支持 Windows")
def test_unix_only():
...
执行时按标签过滤:
bash
pytest -m "not slow" # 跳过慢测试
pytest -m "integration" # 只跑集成测试
自定义 marker 需要在 pytest.ini 里注册,避免警告:
ini
[pytest]
markers =
slow: 标记为耗时测试
integration: 标记为集成测试
九、异常测试与内置工具 fixture
测试异常
python
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
result = 1 / 0
# 还可以验证错误信息
def test_invalid_input():
with pytest.raises(ValueError) as exc_info:
parse_age(-1)
assert "must be positive" in str(exc_info.value)
几个好用的内置 fixture
python
# 捕获 print 输出
def test_output(capsys):
print("hello pytest")
captured = capsys.readouterr()
assert "hello" in captured.out
# 捕获日志
def test_logging(caplog):
import logging
logging.getLogger().info("服务启动")
assert "服务启动" in caplog.text
# 临时文件目录(测试结束自动删除)
def test_file_write(tmp_path):
f = tmp_path / "result.txt"
f.write_text("data")
assert f.read_text() == "data"
十、常用插件生态
pytest 的插件体系是它长盛不衰的核心原因之一。下面是工程实践中最常装的几个:
| 插件 | 功能 | 安装命令 |
|---|---|---|
pytest-cov |
代码覆盖率报告 | pip install pytest-cov |
pytest-xdist |
多核并行执行测试 | pip install pytest-xdist |
pytest-mock |
更简洁的 mock 接口 | pip install pytest-mock |
pytest-asyncio |
支持 async/await 测试 | pip install pytest-asyncio |
pytest-timeout |
检测卡死的测试 | pip install pytest-timeout |
pytest-randomly |
随机化执行顺序,发现隐藏依赖 | pip install pytest-randomly |
并行执行示例,大型项目能显著提速:
arduino
pytest -n auto # 自动使用所有 CPU 核心
异步测试示例(FastAPI 项目必备):
python
import pytest
@pytest.mark.asyncio
async def test_async_fetch():
result = await fetch_data_from_api()
assert result["status"] == "ok"
覆盖率报告:
css
pytest --cov=src --cov-report=html
十一、核心概念关系图
写在最后
学 pytest 最好的方式,是在一个真实项目里从第一个 test_ 函数开始写起。先掌握 fixture 和 parametrize,这两个工具能解决 80% 的日常测试需求;再慢慢引入 mock 和插件,随着项目规模增长自然就知道什么时候需要什么。
测试不是负担,是你对自己代码质量的承诺。写好测试的代码,重构起来才真的有底气。