pytest 高级用法¶
一句话概述:pytest 是 Python 最流行的测试框架,用简单的 assert 语句写测试,支持 fixture 依赖注入、参数化测试、插件扩展,比 unittest 简洁得多。
核心知识点¶
| 概念 | 白话解释 |
|---|---|
| fixture | 测试前的准备工作(比如创建数据库连接),用 @pytest.fixture 装饰 |
| 参数化 | 一个测试函数用不同参数跑多次,@pytest.mark.parametrize |
| 标记(mark) | 给测试打标签,比如 @pytest.mark.slow 标记慢测试 |
| conftest.py | 共享 fixture 的文件,同目录及子目录自动可用 |
| 插件 | 扩展 pytest 功能,比如 pytest-cov(覆盖率)、pytest-mock(mock) |
| 夹具作用域 | fixture 的生命周期:function/class/module/session |
安装配置¶
# 安装 pytest 和常用插件
pip install pytest # 核心框架
pip install pytest-cov # 代码覆盖率
pip install pytest-mock # Mock 工具
pip install pytest-xdist # 并行执行测试
pip install pytest-asyncio # 异步测试支持
pip install pytest-timeout # 超时控制
pip install pytest-html # HTML 报告
配置文件¶
# pyproject.toml - 推荐的配置方式
[tool.pytest.ini_options]
testpaths = ["tests"] # 测试文件目录
python_files = ["test_*.py"] # 测试文件名模式
python_functions = ["test_*"] # 测试函数名模式
addopts = "-v --tb=short --strict-markers" # 默认参数:详细输出、短回溯、严格标记
markers = [ # 自定义标记
"slow: 慢速测试",
"integration: 集成测试",
"e2e: 端到端测试",
]
基本使用¶
简单测试¶
# tests/test_math.py
def add(a, b):
"""加法函数"""
return a + b # 返回两数之和
def test_add():
"""测试加法"""
assert add(1, 2) == 3 # 用 assert 断言结果
assert add(-1, 1) == 0 # 测试负数
assert add(0, 0) == 0 # 测试零
def test_add_float():
"""测试浮点数加法"""
result = add(0.1, 0.2)
assert abs(result - 0.3) < 1e-9 # 浮点数比较要用近似
运行测试¶
pytest # 运行所有测试
pytest tests/test_math.py # 运行指定文件
pytest -k "test_add" # 按名称过滤
pytest -v # 详细输出
pytest -x # 遇到第一个失败就停止
pytest --tb=long # 详细的错误回溯
pytest -s # 显示 print 输出
pytest --co # 只列出测试,不执行
高级用法¶
Fixture 夹具¶
# tests/conftest.py - 共享 fixture(自动被同目录及子目录的测试发现)
import pytest # 导入 pytest
@pytest.fixture # 声明为 fixture
def sample_user():
"""创建一个测试用户"""
return {"id": 1, "name": "张三", "email": "zhang@test.com"} # 返回测试数据
@pytest.fixture
def db_connection():
"""创建数据库连接(带清理)"""
conn = create_connection() # 测试前:建立连接
yield conn # yield 之前是 setup,之后是 teardown
conn.close() # 测试后:关闭连接
@pytest.fixture(scope="session") # session 作用域:整个测试会话只执行一次
def app():
"""创建测试应用(整个测试会话复用)"""
app = create_app(testing=True) # 创建测试应用
yield app
@pytest.fixture(scope="module") # module 作用域:每个模块执行一次
def shared_data():
"""模块级共享数据"""
return load_test_data()
# 使用 fixture(参数名和 fixture 函数名一致就自动注入)
def test_user_name(sample_user): # 自动注入 sample_user fixture
assert sample_user["name"] == "张三" # 使用 fixture 返回的数据
def test_db_query(db_connection): # 自动注入 db_connection fixture
result = db_connection.execute("SELECT 1") # 使用数据库连接
assert result is not None
Fixture 参数化¶
@pytest.fixture(params=["sqlite", "postgresql", "mysql"]) # 每个参数都会运行一遍测试
def db_engine(request): # request.param 获取当前参数
"""测试多种数据库引擎"""
engine = create_engine(request.param) # 创建对应的引擎
yield engine # 返回引擎
engine.dispose() # 测试后清理
def test_insert(db_engine): # 这个测试会跑 3 次(sqlite/postgresql/mysql)
db_engine.insert({"name": "test"})
assert db_engine.count() == 1
参数化测试¶
import pytest
# 基本参数化:一个测试跑多组数据
@pytest.mark.parametrize("input_val, expected", [ # 参数名和值列表
(1, 1), # 正数
(0, 0), # 零
(-1, 1), # 负数
(100, 100), # 大数
])
def test_abs(input_val, expected):
"""测试绝对值"""
assert abs(input_val) == expected # 每组参数跑一次
# 多参数组合
@pytest.mark.parametrize("a", [1, 2, 3]) # a 有 3 个值
@pytest.mark.parametrize("b", [10, 20]) # b 有 2 个值
def test_multiply(a, b): # 会跑 3×2=6 次
assert a * b == a * b
# 带 ID 的参数化(让测试报告更清晰)
@pytest.mark.parametrize("email, valid", [
pytest.param("test@example.com", True, id="正常邮箱"),
pytest.param("invalid", False, id="无效格式"),
pytest.param("", False, id="空字符串"),
pytest.param("a@b.c", True, id="最短合法邮箱"),
])
def test_email_validation(email, valid):
assert is_valid_email(email) == valid
标记(Mark)¶
import pytest
@pytest.mark.slow # 标记为慢测试
def test_large_data():
"""处理大量数据的测试"""
data = generate_large_dataset(1000000)
result = process(data)
assert len(result) > 0
@pytest.mark.integration # 标记为集成测试
def test_api_call():
"""需要真实 API 的测试"""
response = call_external_api()
assert response.status == 200
@pytest.mark.skip(reason="功能还没实现") # 跳过测试
def test_future_feature():
pass
@pytest.mark.skipif( # 条件跳过
sys.platform == "win32",
reason="只在 Linux 上跑"
)
def test_linux_only():
pass
@pytest.mark.xfail(reason="已知 bug,等修复") # 预期失败
def test_known_bug():
assert buggy_function() == "correct"
# 按标记运行
pytest -m slow # 只跑标记为 slow 的
pytest -m "not slow" # 跳过 slow 的
pytest -m "integration or e2e" # 跑集成或 E2E 测试
Mock 模拟¶
from unittest.mock import patch, MagicMock
import pytest
# 方式一:pytest-mock(推荐)
def test_api_call(mocker): # mocker 是 pytest-mock 提供的 fixture
# 模拟 requests.get 返回假数据
mock_response = mocker.Mock() # 创建 mock 对象
mock_response.json.return_value = {"name": "张三"} # 设置返回值
mock_response.status_code = 200
mocker.patch("requests.get", return_value=mock_response) # 替换 requests.get
result = fetch_user(1) # 这里调用的 requests.get 是 mock 的
assert result["name"] == "张三"
# 方式二:标准库 patch
@patch("myapp.db.get_user") # 替换指定函数
def test_with_patch(mock_get_user):
mock_get_user.return_value = {"id": 1, "name": "李四"} # 设置返回值
user = get_user_info(1)
assert user["name"] == "李四"
mock_get_user.assert_called_once_with(1) # 验证被调用了一次且参数是 1
异步测试¶
import pytest
import asyncio
@pytest.mark.asyncio # 标记为异步测试
async def test_async_function():
"""测试异步函数"""
result = await async_fetch_data() # await 异步函数
assert result["status"] == "ok"
@pytest.mark.asyncio
async def test_async_timeout():
"""测试异步超时"""
with pytest.raises(asyncio.TimeoutError): # 断言抛出超时错误
await asyncio.wait_for(slow_function(), timeout=1.0)
覆盖率¶
# 运行测试并生成覆盖率报告
pytest --cov=src # 统计 src 目录的覆盖率
pytest --cov=src --cov-report=html # 生成 HTML 报告
pytest --cov=src --cov-report=term-missing # 终端显示未覆盖的行号
pytest --cov=src --cov-fail-under=80 # 覆盖率低于 80% 则失败
常见报错¶
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
ModuleNotFoundError | 导入路径不对 | 在项目根目录跑 pytest,或加 conftest.py |
fixture 'xxx' not found | fixture 没定义或不在作用域 | 检查 conftest.py 位置和 fixture 名称 |
PytestUnraisableExceptionWarning | 异步资源没清理 | 在 fixture 的 yield 后加清理代码 |
FAILED (xfail) | 标记了 xfail 但实际通过了 | 移除 xfail 标记(bug 已修复) |
marker not defined | 自定义标记没注册 | 在 pyproject.toml 的 markers 里注册 |
| 测试发现不了 | 文件名/函数名不符合约定 | 文件名 test_*.py,函数名 test_* |
速查表¶
# CLI 命令
pytest # 运行所有测试
pytest -v # 详细输出
pytest -x # 第一个失败就停
pytest -k "关键字" # 按名称过滤
pytest -m "标记" # 按标记过滤
pytest --lf # 只跑上次失败的
pytest --ff # 先跑上次失败的
pytest -n auto # 并行执行(需要 pytest-xdist)
pytest --cov=src # 覆盖率
pytest --html=report.html # HTML 报告
pytest --timeout=60 # 超时控制
# 常用断言
assert x == y # 等于
assert x != y # 不等于
assert x in collection # 在集合中
assert x is None # 是 None
assert isinstance(x, Type) # 是某类型
# 异常断言
with pytest.raises(ValueError): # 断言抛出 ValueError
raise ValueError("xxx")
with pytest.raises(ValueError, match="xxx"): # 还要匹配消息
# 近似断言
assert x == pytest.approx(3.14, abs=0.01) # 近似相等
# fixture 作用域
@pytest.fixture(scope="function") # 每个测试函数(默认)
@pytest.fixture(scope="class") # 每个测试类
@pytest.fixture(scope="module") # 每个模块
@pytest.fixture(scope="session") # 整个测试会话
参考:pytest 官方文档 | pytest-cov | pytest-mock