目录
pytest 是 Python 生态中最流行、功能最强大的测试框架之一,它提供了简洁优雅的语法和丰富灵活的测试功能。下面我们将详细介绍 pytest 的核心概念和实用技巧。
1. 安装与基础配置
安装方法
使用 pip 安装最新版本 pytest:
pip install pytest
对于开发环境,建议同时安装常用插件:
pip install pytest pytest-cov pytest-xdist pytest-mock
版本检查
安装完成后可以检查版本:
pytest --version
配置文件
pytest 支持通过 pytest.ini
文件进行配置,例如:
[pytest]
addopts = -v --tb=short
python_files = test_*.py
python_classes = Test*
python_functions = test_*
2. 编写测试函数
基本结构
测试函数应遵循以下命名约定:
- 测试文件:
test_*.py
或*_test.py
- 测试函数:
test_*()
- 测试类:
Test*
示例测试文件 test_calculator.py
:
def add(a, b):
"""加法函数"""
return a + b
def test_add_integers():
"""测试整数加法"""
assert add(2, 3) == 5
assert add(-1, 1) == 0
def test_add_floats():
"""测试浮点数加法"""
assert add(0.1, 0.2) == pytest.approx(0.3)
断言机制
pytest 支持所有标准 Python 断言,并提供增强功能:
# 数值比较
assert result == expected
assert abs(result - expected) < 0.001
# 容器测试
assert item in collection
assert collection1 == collection2
# 异常测试
with pytest.raises(ValueError):
function_that_raises()
# 使用 pytest.approx 处理浮点数比较
assert 0.1 + 0.2 == pytest.approx(0.3)
3. 测试执行与报告
基本执行方式
# 运行所有测试
pytest
# 运行特定测试文件
pytest test_module.py
# 运行特定测试函数
pytest test_module.py::test_function
# 运行特定测试类
pytest test_module.py::TestClass
常用命令行选项
选项 | 说明 |
---|---|
-v | 详细输出 |
-x | 遇到第一个失败就停止 |
--maxfail=n | 允许最多 n 次失败 |
-k "expression" | 按名称筛选测试 |
--durations=n | 显示最慢的 n 个测试 |
--lf | 只运行上次失败的测试 |
--ff | 先运行失败的测试 |
--cov | 生成覆盖率报告 |
测试报告
生成 HTML 测试报告:
pytest --html=report.html
生成 JUnit 格式报告:
pytest --junitxml=report.xml
4. 测试组织与管理
测试类
将相关测试组织在类中:
class TestMathOperations:
"""测试数学运算"""
def test_addition(self):
assert 1 + 1 == 2
def test_subtraction(self):
assert 5 - 3 == 2
def test_multiplication(self):
assert 2 * 3 == 6
模块化测试
可以使用 Python 包结构组织测试:
tests/
├── __init__.py
├── unit/
│ ├── test_math.py
│ └── test_string.py
└── integration/
├── test_database.py
└── test_api.py
5. 高级测试功能
Fixtures 详解
Fixtures 提供测试所需资源并处理清理工作:
import pytest
@pytest.fixture(scope="module")
def database_connection():
"""创建数据库连接"""
conn = create_db_connection()
yield conn # 测试函数会在此处运行
conn.close() # 测试完成后清理
@pytest.fixture
def temporary_file(tmp_path):
"""使用内置 tmp_path fixture 创建临时文件"""
file = tmp_path / "test.txt"
file.write_text("test data")
return file
def test_database_query(database_connection):
result = database_connection.query("SELECT * FROM users")
assert len(result) > 0
def test_file_operations(temporary_file):
assert temporary_file.read_text() == "test data"
参数化测试
使用 @pytest.mark.parametrize
测试多组数据:
import pytest
@pytest.mark.parametrize(
"input,expected",
[
("3+5", 8),
("2*4", 8),
("6/2", 3),
("10-2", 8),
]
)
def test_eval(input, expected):
assert eval(input) == expected
异常测试进阶
import pytest
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError) as excinfo:
1 / 0
assert str(excinfo.value) == "division by zero"
@pytest.mark.parametrize(
"value,exception",
[
(None, TypeError),
("string", ValueError),
(-1, ValueError)
]
)
def test_sqrt(value, exception):
import math
with pytest.raises(exception):
math.sqrt(value)
6. 测试控制与标记
跳过测试
import sys
import pytest
@pytest.mark.skip(reason="功能尚未实现")
def test_unimplemented():
assert False
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要 Python 3.8+")
def test_new_syntax():
assert (x := 5) == 5
标记测试
@pytest.mark.slow
def test_long_running():
import time
time.sleep(10)
assert True
@pytest.mark.integration
def test_database_integration():
assert database_is_available()
运行标记的测试:
pytest -m "slow or integration"
7. 插件生态系统
pytest 的强大之处在于其丰富的插件生态系统:
常用插件
-
pytest-cov:代码覆盖率测试
pytest --cov=myproject --cov-report=html
-
pytest-xdist:并行测试
pytest -n 4 # 使用4个CPU核心
-
pytest-mock:模拟对象
def test_api_call(mocker): mocker.patch("requests.get", return_value={"status": "ok"}) assert call_api() == "ok"
-
pytest-django:Django项目测试
-
pytest-asyncio:异步测试
-
pytest-timeout:测试超时控制
8. 最佳实践
- 测试隔离:每个测试应该独立运行,不依赖其他测试的状态
- 描述性命名:使用清晰的测试名称说明测试目的
- 单一断言原则:每个测试最好只验证一个行为
- 避免测试实现细节:测试公共接口而不是内部实现
- 使用 fixtures:共享测试资源,减少重复代码
- 定期运行测试:集成到持续集成(CI)流程中
9. 完整示例项目
以下是一个典型的项目结构和使用 pytest 的示例:
myproject/
├── src/
│ └── calculator.py
├── tests/
│ ├── __init__.py
│ ├── unit/
│ │ ├── test_math.py
│ │ └── test_operations.py
│ └── integration/
│ └── test_api.py
├── pytest.ini
└── requirements.txt
src/calculator.py
:
class Calculator:
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
def multiply(self, a, b):
return a * b
def divide(self, a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
tests/unit/test_math.py
:
import pytest
from src.calculator import Calculator
@pytest.fixture
def calculator():
return Calculator()
class TestCalculator:
def test_add(self, calculator):
assert calculator.add(2, 3) == 5
def test_subtract(self, calculator):
assert calculator.subtract(5, 3) == 2
def test_multiply(self, calculator):
assert calculator.multiply(2, 3) == 6
def test_divide(self, calculator):
assert calculator.divide(6, 3) == 2
def test_divide_by_zero(self, calculator):
with pytest.raises(ValueError, match="Cannot divide by zero"):
calculator.divide(1, 0)
10. 学习资源
通过掌握 pytest 的这些功能,你将能够为 Python 项目编写高效、可维护的测试代码,显著提高代码质量和开发效率。