刚接手一个新项目时,最头疼的就是如何快速搭建可靠的测试体系。我见过太多项目因为缺乏良好的测试覆盖,导致每次修改代码都像在走钢丝。而Pytest就像给你的代码系上了安全带,它能让你在修改代码时心里有底。
记得我第一次用Pytest时,就被它的简洁震惊了。原本需要几十行代码才能完成的测试,用Pytest可能只需要几行。比如测试一个简单的加法函数:
python复制def test_add():
assert 1 + 1 == 2
就这么简单!不需要继承任何类,不需要记住各种断言方法,一个普通的Python函数加上assert语句就搞定了。这种直观的写法让我从此爱上了写测试。
安装Pytest简单到令人发指:
bash复制pip install pytest
但真正让Pytest强大的是它的插件系统。我建议一开始就安装这几个必备插件:
bash复制pip install pytest-cov pytest-xdist pytest-html
配置方面,我习惯在项目根目录下创建pytest.ini文件:
ini复制[pytest]
addopts = -v --cov=src --cov-report=html
python_files = test_*.py
这样每次运行测试都会自动生成覆盖率报告,而且只查找test_开头的测试文件。
好的项目结构能让测试事半功倍。我推荐这样组织:
code复制project/
├── src/ # 源代码
├── tests/ # 测试代码
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── conftest.py # 共享fixture
├── pytest.ini # 配置文件
└── requirements.txt
特别要注意conftest.py这个文件,它是Pytest的魔法文件,可以存放项目中所有测试共享的fixture。
假设我们有个计算器类:
python复制# src/calculator.py
class Calculator:
def add(self, a, b):
return a + b
def divide(self, a, b):
if b == 0:
raise ValueError("除数不能为零")
return a / b
对应的测试可以这样写:
python复制# tests/unit/test_calculator.py
from src.calculator import Calculator
def test_add():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_divide():
calc = Calculator()
assert calc.divide(6, 3) == 2
测试异常情况也很简单:
python复制import pytest
def test_divide_by_zero():
calc = Calculator()
with pytest.raises(ValueError) as excinfo:
calc.divide(1, 0)
assert "除数不能为零" in str(excinfo.value)
pytest.raises可以捕获预期的异常,还能检查异常信息是否符合预期。
当需要测试多组输入时,参数化是救星:
python复制@pytest.mark.parametrize("a,b,expected", [
(1, 1, 2),
(2, 3, 5),
(100, 200, 300)
])
def test_add_parametrize(a, b, expected):
assert Calculator().add(a, b) == expected
这样同一个测试会运行三次,分别测试不同的输入组合。
Fixture是Pytest最强大的功能之一。比如我们需要在每个测试前创建一个新的Calculator实例:
python复制@pytest.fixture
def calculator():
return Calculator()
def test_add_with_fixture(calculator):
assert calculator.add(2, 3) == 5
更复杂的fixture还可以管理数据库连接:
python复制@pytest.fixture
def db_connection():
conn = create_db_connection()
yield conn # 测试执行时使用这个conn
conn.close() # 测试结束后清理
配置好pytest-cov后,运行:
bash复制pytest --cov=src --cov-report=term-missing
这会显示哪些代码没有被测试覆盖,帮助我们查漏补缺。
对于大型测试套件,使用pytest-xdist可以大幅缩短测试时间:
bash复制pytest -n auto # 自动使用所有CPU核心
bash复制pytest --html=report.html
生成的报告包含测试结果概览、失败详情等,非常适合团队分享。
有时测试需要依赖外部服务,这时可以用标记来分类:
python复制@pytest.mark.integration
def test_api_call():
# 测试外部API
pass
然后可以单独运行:
bash复制pytest -m "not integration" # 跳过集成测试
对于偶发失败的测试,可以用flaky标记:
python复制@pytest.mark.flaky(reruns=3)
def test_unstable_api():
# 如果失败会自动重试3次
pass
我习惯把测试数据放在单独的JSON或YAML文件中:
python复制import json
@pytest.fixture
def test_data():
with open("tests/data/test_cases.json") as f:
return json.load(f)
def test_with_data(test_data):
for case in test_data:
assert some_function(case["input"]) == case["expected"]
当现有功能不够用时,可以自己写插件。比如这个简单的插件会在测试开始时打印信息:
python复制# conftest.py
def pytest_runtest_logstart(nodeid, location):
print(f"\n开始测试: {nodeid}")
更复杂的插件可以修改测试行为、添加命令行选项等。Pytest的插件系统非常灵活,几乎可以扩展任何功能。
成熟的测试流程应该集成到CI中。这是GitHub Actions的配置示例:
yaml复制name: Python Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install dependencies
run: pip install pytest pytest-cov
- name: Run tests
run: pytest --cov=src --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v1
这样每次代码提交都会自动运行测试并上传覆盖率报告。
Pytest也可以做简单的性能测试:
python复制@pytest.mark.benchmark
def test_performance(benchmark):
result = benchmark(lambda: some_heavy_function())
assert result < 100 # 执行时间应小于100ms
配合pytest-benchmark插件,可以得到详细的性能分析报告。
在实际项目中,我建议采用分层测试策略:
Pytest可以很好地支持所有这些测试类型。关键是保持测试的快速反馈和可维护性。