1. pytest 测试框架概述
pytest 是 Python 生态中最流行的测试框架之一,它以其简洁的语法和强大的功能赢得了开发者的青睐。不同于 Python 自带的 unittest 模块,pytest 提供了更灵活的测试编写方式,支持丰富的插件扩展,并且能够轻松处理各种复杂的测试场景。
我第一次接触 pytest 是在一个 Web 开发项目中,当时团队正面临测试用例维护困难的问题。传统的 unittest 写法让测试代码变得冗长,而 pytest 的简洁语法让我们能够专注于测试逻辑本身,而不是繁琐的样板代码。从那时起,pytest 就成了我所有 Python 项目的标配测试工具。
pytest 的核心优势在于:
- 无需继承任何基类,普通函数加上 assert 语句就能成为测试用例
- 自动发现测试文件和测试函数
- 丰富的断言内省,失败时提供详细上下文信息
- 灵活的 fixture 系统,完美解决测试依赖和资源管理问题
- 庞大的插件生态系统,轻松扩展功能
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与安装
2.1 安装 pytest
安装 pytest 非常简单,使用 pip 即可完成:
bash复制pip install pytest
为了验证安装是否成功,可以运行:
bash复制pytest --version
这应该会输出已安装的 pytest 版本信息。我建议同时安装一些常用插件,它们能显著提升测试体验:
bash复制pip install pytest-cov pytest-xdist pytest-mock
- pytest-cov:生成测试覆盖率报告
- pytest-xdist:支持并行运行测试
- pytest-mock:简化 mock 操作
提示:最好在虚拟环境中安装 pytest 及其插件,避免污染全局 Python 环境。可以使用 venv 或 conda 创建隔离环境。
2.2 项目结构建议
良好的项目结构对测试管理至关重要。我通常采用如下布局:
code复制project_root/
├── src/ # 源代码目录
│ └── module.py # 业务代码
└── tests/ # 测试目录
├── __init__.py # 使 tests 成为 Python 包
├── conftest.py # 全局 fixture 定义
├── test_module.py # 测试文件
└── functional/ # 功能测试目录
这种结构清晰分离了源代码和测试代码,同时遵循了 Python 的包管理规范。conftest.py 是 pytest 的特殊文件,用于存放项目级的 fixture,我们稍后会详细介绍。
3. 编写第一个测试
3.1 基本测试示例
让我们从一个简单的测试开始。假设我们有一个计算阶乘的函数:
python复制# src/math_operations.py
def factorial(n):
if n < 0:
raise ValueError("Factorial is not defined for negative numbers")
return 1 if n <= 1 else n * factorial(n-1)
对应的测试文件可以这样写:
python复制# tests/test_math_operations.py
from src.math_operations import factorial
def test_factorial_of_zero():
assert factorial(0) == 1
def test_factorial_of_one():
assert factorial(1) == 1
def test_factorial_of_positive_number():
assert factorial(5) == 120
def test_factorial_raises_for_negative():
import pytest
with pytest.raises(ValueError):
factorial(-1)
这个简单的例子展示了 pytest 的几个关键特性:
- 测试函数以
test_开头(这是 pytest 的默认发现规则) - 使用普通的
assert语句进行断言 pytest.raises用于验证异常抛出- 不需要任何类继承或特殊方法
3.2 运行测试
在项目根目录下,直接运行:
bash复制pytest
pytest 会自动发现并运行所有测试。你会看到类似这样的输出:
code复制============================= test session starts ==============================
platform linux -- Python 3.9.0, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /path/to/project
collected 4 items
tests/test_math_operations.py .... [100%]
============================== 4 passed in 0.02s ===============================
每个点代表一个通过的测试。如果有测试失败,pytest 会提供详细的失败信息,包括哪个断言失败了,期望值是什么,实际值是什么。
4. 高级测试特性
4.1 参数化测试
当我们需要用不同输入测试相同逻辑时,参数化测试能显著减少重复代码。继续以阶乘函数为例:
python复制import pytest
from src.math_operations import factorial
@pytest.mark.parametrize("input,expected", [
(0, 1),
(1, 1),
(5, 120),
(10, 3628800),
])
def test_factorial(input, expected):
assert factorial(input) == expected
@pytest.mark.parametrize 装饰器允许我们定义一个测试函数的多个输入组合。pytest 会为每组参数单独运行一次测试,并在报告中分别显示结果。
经验分享:参数化测试特别适合验证边界条件和各种输入组合。我通常会把正常情况、边界情况和异常情况的测试都放在同一个参数化测试中,这样能更清晰地看到函数的完整行为。
4.2 Fixture 系统
Fixture 是 pytest 最强大的功能之一,它提供了一种优雅的方式来管理测试资源和依赖。
基本 fixture 示例
假设我们有一个需要数据库连接的函数:
python复制# src/db_operations.py
def get_user_count(db_conn):
cursor = db_conn.cursor()
cursor.execute("SELECT COUNT(*) FROM users")
return cursor.fetchone()[0]
我们可以创建一个 fixture 来管理数据库连接:
python复制# tests/conftest.py
import pytest
import sqlite3
@pytest.fixture
def db_connection():
conn = sqlite3.connect(":memory:")
# 初始化测试数据库
cursor = conn.cursor()
cursor.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
cursor.execute("INSERT INTO users (name) VALUES ('Alice'), ('Bob')")
conn.commit()
yield conn
conn.close()
然后在测试中使用这个 fixture:
python复制# tests/test_db_operations.py
from src.db_operations import get_user_count
def test_user_count(db_connection):
assert get_user_count(db_connection) == 2
Fixture 的生命周期
Fixture 可以通过 scope 参数控制生命周期:
python复制@pytest.fixture(scope="module")
def expensive_resource():
# 这个 fixture 在整个测试模块中只会初始化一次
resource = se
