Pytest测试框架 — Python Pytest测试入门与进阶
一句话概述:Pytest 是 Python 最流行的测试框架,用简单的 assert 语句替代复杂的 self.assertEqual,支持参数化、fixture、插件,让写测试像写普通函数一样轻松。
核心知识点速查表
| 概念 | 白话解释 |
|---|
| assert 断言 | 就是"我断定这个结果应该是XX",不对就报错 |
| fixture | 测试的"准备工作",比如创建数据库连接、准备测试数据 |
| 参数化 | 一个测试函数,自动跑多组不同数据 |
| conftest.py | 放公共 fixture 的文件,同目录下的测试自动能用 |
| marker 标记 | 给测试打标签,比如标记为"慢测试",可以单独跑或跳过 |
| 插件 | 扩展 pytest 功能的组件,比如 pytest-cov 算覆盖率 |
当前版本信息(2026年)
| 信息 | 详情 |
|---|
| 最新版本 | Pytest 9.0.2(2026年4月7日) |
| Python 要求 | Python 3.9+ |
| 官网 | https://docs.pytest.org |
| GitHub | https://github.com/pytest-dev/pytest |
| 内置规则数 | 850+ 外部插件 |
安装配置
基本安装
# 用 pip 安装 pytest(推荐用虚拟环境)
pip install pytest
# 用 uv 安装(更快)
uv pip install pytest
# 验证安装成功
pytest --version # 应该显示 pytest 9.0.x
常用插件安装
# 覆盖率插件(看你的代码被测了多少)
pip install pytest-cov
# 异步测试支持(测试 async 函数用的)
pip install pytest-asyncio
# HTML 测试报告(生成漂亮的网页报告)
pip install pytest-html
# 并行执行(多个测试同时跑,加速)
pip install pytest-xdist
# mock 模拟(模拟外部依赖,比如数据库、API)
pip install pytest-mock
配置文件(pyproject.toml)
# 在 pyproject.toml 中配置 pytest(推荐方式)
[tool.pytest.ini_options]
minversion = "9.0" # 最低 pytest 版本要求
testpaths = ["tests"] # 测试文件放在 tests 目录
python_files = "test_*.py" # 测试文件命名规则
python_functions = "test_*" # 测试函数命名规则
addopts = "-v --tb=short" # 默认参数:详细模式 + 简短回溯
markers = [ # 自定义标记
"slow: 运行慢的测试",
"integration: 集成测试",
]
基本使用
第一个测试
# tests/test_calculator.py
# 测试文件必须以 test_ 开头
def add(a, b):
"""简单的加法函数"""
return a + b # 返回两数之和
def test_add_positive():
"""测试正数相加"""
result = add(1, 2) # 调用被测函数
assert result == 3 # 断言结果应该等于3
def test_add_negative():
"""测试负数相加"""
assert add(-1, -2) == -3 # 负数相加
def test_add_zero():
"""测试加零"""
assert add(5, 0) == 5 # 任何数加零等于自身
运行测试
# 运行所有测试(在项目根目录执行)
pytest
# 运行指定文件
pytest tests/test_calculator.py
# 运行指定函数
pytest tests/test_calculator.py::test_add_positive
# 详细输出(显示每个测试的名字和结果)
pytest -v
# 遇到第一个失败就停止(调试时很有用)
pytest -x
# 只运行上次失败的测试
pytest --lf
# 显示 print 输出(默认被 pytest 捕获了)
pytest -s
Fixture(测试准备工作)
# tests/conftest.py
# conftest.py 放公共 fixture,同目录所有测试文件自动可用
import pytest
@pytest.fixture # 装饰器声明这是一个 fixture
def sample_data():
"""准备测试数据"""
return { # 返回一个字典作为测试数据
"name": "张三",
"age": 25,
"scores": [85, 92, 78]
}
@pytest.fixture
def temp_file(tmp_path):
"""创建临时测试文件(tmp_path 是 pytest 内置 fixture)"""
file = tmp_path / "test.txt" # 在临时目录创建文件
file.write_text("hello world") # 写入测试内容
return file # 返回文件路径给测试使用
# tests/test_data.py
def test_name(sample_data):
"""fixture 会自动注入到参数中"""
assert sample_data["name"] == "张三" # 使用 fixture 提供的数据
def test_scores_avg(sample_data):
"""测试平均分计算"""
scores = sample_data["scores"] # 取出分数列表
avg = sum(scores) / len(scores) # 计算平均分
assert avg == 85.0 # 断言平均分
参数化测试
import pytest
# @pytest.mark.parametrize 让一个测试函数跑多组数据
@pytest.mark.parametrize("input_val, expected", [
(1, 1), # 1的阶乘是1
(2, 2), # 2的阶乘是2
(3, 6), # 3的阶乘是6
(4, 24), # 4的阶乘是24
(5, 120), # 5的阶乘是120
])
def test_factorial(input_val, expected):
"""测试阶乘函数,5组数据会产生5个独立测试"""
from math import factorial # 导入阶乘函数
assert factorial(input_val) == expected # 断言每组结果
高级用法
Fixture 作用域与清理
import pytest
@pytest.fixture(scope="session") # session 级别:整个测试过程只执行一次
def db_connection():
"""模拟数据库连接(整个测试过程只连一次)"""
print(">>> 连接数据库")
conn = {"host": "localhost", "connected": True} # 模拟连接
yield conn # yield 之前是 setup,之后是 teardown
print(">>> 断开数据库") # 所有测试结束后执行清理
conn["connected"] = False
@pytest.fixture(scope="function") # function 级别:每个测试函数都执行(默认)
def clean_table(db_connection):
"""每个测试前清空表"""
print("清空测试表")
yield # 测试执行
print("回滚数据") # 测试结束后回滚
# scope 的4个级别(从小到大):
# function — 每个测试函数执行一次(默认)
# class — 每个测试类执行一次
# module — 每个测试文件执行一次
# session — 整个测试过程执行一次
异常测试
import pytest
def divide(a, b):
"""除法函数"""
if b == 0:
raise ValueError("除数不能为零") # 除数为零时抛异常
return a / b
def test_divide_by_zero():
"""测试除以零应该抛出 ValueError"""
with pytest.raises(ValueError) as exc_info: # 捕获预期的异常
divide(10, 0) # 这行应该抛异常
assert "除数不能为零" in str(exc_info.value) # 检查异常消息
def test_divide_normal():
"""测试正常除法"""
assert divide(10, 2) == 5.0 # 正常情况应该返回结果
Mock 模拟
# tests/test_api.py
from unittest.mock import patch, MagicMock
def fetch_user(user_id):
"""假设这个函数会调用真实的 API"""
import requests
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
def test_fetch_user_mock():
"""用 mock 模拟 API 调用,不需要真的发网络请求"""
mock_response = MagicMock() # 创建模拟对象
mock_response.json.return_value = {"id": 1, "name": "测试用户"} # 设置返回值
with patch("requests.get", return_value=mock_response): # 替换 requests.get
result = fetch_user(1) # 调用被测函数(不会真的发请求)
assert result["name"] == "测试用户" # 验证结果
覆盖率报告
# 运行测试并计算覆盖率
pytest --cov=src --cov-report=html
# --cov=src 指定要统计覆盖率的源码目录
# --cov-report=html 生成 HTML 报告(在 htmlcov/ 目录)
# 打开 htmlcov/index.html 查看详细覆盖率
# 设置最低覆盖率(低于80%则测试失败)
pytest --cov=src --cov-fail-under=80
标记与过滤
import pytest
@pytest.mark.slow # 标记为慢测试
def test_big_data_processing():
"""处理大量数据的测试(耗时较长)"""
import time
time.sleep(2) # 模拟耗时操作
assert True
@pytest.mark.skipif( # 条件跳过
condition=True,
reason="功能还没实现"
)
def test_not_ready():
"""这个测试会被跳过"""
pass
@pytest.mark.xfail(reason="已知 bug") # 预期失败
def test_known_bug():
"""这个测试预期会失败,不会算作错误"""
assert 1 + 1 == 3
# 只运行标记为 slow 的测试
pytest -m slow
# 跳过标记为 slow 的测试
pytest -m "not slow"
# 只运行集成测试
pytest -m integration
Pytest 9.x 新特性
# 原生 TOML 配置支持(Pytest 9.0 新增)
# 以前只能在 [tool.pytest.ini_options] 下用 INI 兼容格式
# 现在可以用原生 TOML 格式
[tool.pytest]
minversion = "9.0"
testpaths = ["tests"]
# 终端进度条(9.0 新增,部分终端可用)
# 在终端标签页显示测试进度
# 手动启用:pytest -p terminalprogress
# 核心 Subtests 支持(9.0 新增,原来是插件)
# 一个测试函数内可以有多个子测试
常见报错与解决
| 报错信息 | 原因 | 解决方案 |
|---|
ModuleNotFoundError | 找不到被测模块 | 在项目根目录运行,或加 __init__.py,或配置 pythonpath |
collected 0 items | 没找到测试 | 检查文件名是否以 test_ 开头,函数名是否以 test_ 开头 |
fixture not found | fixture 找不到 | 检查 conftest.py 位置,fixture 名是否拼对 |
FAILED assert ... | 断言失败 | 看 pytest 输出的详细对比,找出实际值和期望值的差异 |
PytestUnhandledCoroutineWarning | 异步测试没加标记 | 安装 pytest-asyncio 并加 @pytest.mark.asyncio |
fixture scope mismatch | fixture 作用域冲突 | 低作用域 fixture 不能依赖高作用域的,调整 scope |
常见问题处理
# 问题1:ModuleNotFoundError
# 在 pyproject.toml 中添加:
# [tool.pytest.ini_options]
# pythonpath = ["src"]
# 问题2:测试发现不了
# 确保文件结构正确:
# project/
# ├── src/
# │ └── mymodule.py
# ├── tests/
# │ ├── __init__.py # 这个文件很重要!
# │ └── test_mymodule.py
# └── pyproject.toml
# 问题3:fixture 冲突
# 检查 conftest.py 层级
# project/
# ├── conftest.py # 全局 fixture
# └── tests/
# ├── conftest.py # 测试目录级 fixture
# ├── unit/
# │ └── conftest.py # 单元测试级 fixture
# └── integration/
# └── conftest.py # 集成测试级 fixture
速查表
# ===== 运行命令 =====
pytest # 运行所有测试
pytest -v # 详细模式
pytest -x # 遇到失败立即停止
pytest -s # 显示 print 输出
pytest --lf # 只跑上次失败的
pytest --ff # 先跑上次失败的,再跑其他
pytest -k "add" # 按名字过滤(包含 add 的)
pytest -m slow # 按标记过滤
pytest --co # 只列出测试,不运行
pytest -n auto # 并行运行(需要 pytest-xdist)
pytest --cov=src # 统计覆盖率
# ===== 常用断言 =====
assert x == y # 相等
assert x != y # 不等
assert x > y # 大于
assert x in collection # 包含
assert isinstance(x, int) # 类型检查
assert not x # 为假
pytest.raises(ValueError) # 预期抛异常
pytest.approx(0.1 + 0.2, 0.3) # 浮点数近似相等
# ===== Fixture 装饰器 =====
@pytest.fixture # 函数级 fixture
@pytest.fixture(scope="session") # 会话级 fixture
@pytest.fixture(autouse=True) # 自动使用(不需要参数注入)
@pytest.fixture(params=[1,2,3]) # 参数化 fixture
# ===== 标记装饰器 =====
@pytest.mark.skip(reason="...") # 无条件跳过
@pytest.mark.skipif(cond, reason="...") # 条件跳过
@pytest.mark.xfail(reason="...") # 预期失败
@pytest.mark.parametrize("a,b", [...]) # 参数化
@pytest.mark.slow # 自定义标记
同类工具对比
| 特性 | Pytest | unittest | nose2 |
|---|
| 断言方式 | 简单 assert | self.assertEqual | assert |
| Fixture | 装饰器 + 注入 | setUp/tearDown | setUp/tearDown |
| 参数化 | 内置 | 需要 subTest | 内置 |
| 插件生态 | 850+ 插件 | 较少 | 中等 |
| 学习难度 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 社区活跃度 | 非常高 | 中等(标准库) | 低(维护模式) |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
总结:Pytest 是 2026 年 Python 测试的事实标准。如果你是新手,直接学 Pytest 就对了,unittest 只需要能看懂老项目代码即可。Pytest 9.0 带来了原生 TOML 配置和核心 subtests 支持,生态更加完善。