1. 为什么选择Pytest作为Python测试框架
在Python生态系统中,测试框架的选择其实不少,比如标准库自带的unittest、nose等。但为什么越来越多的开发者转向Pytest?这要从我五年前的一个项目说起。当时我们团队接手了一个遗留系统,测试覆盖率不足30%,每次发布都提心吊胆。尝试了各种测试工具后,最终Pytest让我们在三个月内将覆盖率提升到了85%以上。
Pytest的核心优势在于它的"约定优于配置"哲学。与需要继承特定类的unittest不同,Pytest只需要你的测试函数以test_开头,就能自动发现并运行测试。这种极简的设计让编写测试变得异常轻松。举个例子,下面是一个最基础的测试对比:
python复制# unittest风格
import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(1 + 1, 2)
# Pytest风格
def test_addition():
assert 1 + 1 == 2
可以看到,Pytest版本少了7行模板代码。当项目中有数百个测试用例时,这种简洁性带来的效率提升是惊人的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建与基础配置
2.1 安装与版本选择
虽然安装Pytest只需要简单的pip install pytest,但在实际项目中,我强烈建议使用虚拟环境并固定版本。这是我用血的教训换来的经验:曾经因为一个团队成员使用了不同版本的Pytest,导致参数化测试行为不一致,浪费了两天排查时间。
推荐的做法是:
bash复制python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
pip install pytest==7.4.0 # 截至2023年10月的稳定版本
对于企业级项目,还应该将依赖项写入requirements.txt或Pipfile中。我个人偏好使用pip-tools来管理精确的依赖关系:
bash复制# requirements.in
pytest>=7.0,<8.0
pytest-cov
# 编译为精确版本
pip-compile requirements.in
2.2 项目结构规范
经过多个项目的实践,我总结出了一个高效的测试目录结构:
code复制project_root/
│
├── src/ # 源代码
│ └── your_package/
│ ├── __init__.py
│ └── module.py
│
├── tests/ # 测试代码
│ ├── unit/ # 单元测试
│ │ └── test_module.py
│ ├── integration/ # 集成测试
│ └── functional/ # 功能测试
│
├── conftest.py # 全局fixture
└── pytest.ini # 配置文件
关键点说明:
src目录使用显式命名而非直接使用包名,避免导入混淆- 测试目录与源码目录分离,便于打包时排除测试代码
- 使用
conftest.py存放项目级fixture,可以被所有测试模块共享
2.3 配置文件详解
一个完整的pytest.ini配置示例:
ini复制[pytest]
testpaths = tests
python_files = test_*.py
python_functions = test_*
addopts =
--verbose
--color=yes
--cov=src/your_package
--cov-report=term-missing
-p no:warnings
junit_family = xunit2
filterwarnings =
ignore::DeprecationWarning
配置解析:
testpaths:指定测试目录,支持多个路径用空格分隔addopts:默认命令行选项--cov:启用覆盖率统计--cov-report:显示未覆盖的行号-p no:warnings:忽略警告信息
junit_family:兼容Jenkins等CI系统的报告格式filterwarnings:过滤特定警告
注意:在团队协作项目中,建议将覆盖率阈值设为CI流程的通过条件。可以在
pyproject.toml中添加:toml复制[tool.pytest.ini_options] min_cov = 80
3. 测试编写实战技巧
3.1 断言的艺术
Pytest的断言之所以强大,是因为它重写了Python的assert语句,提供了丰富的失败信息。但很多开发者没有充分利用这个特性。来看一个实际案例:
python复制# 不够好的写法
def test_user_creation():
user = create_user("test@example.com")
assert user is not None
assert user.email == "test@example.com"
assert user.is_active is True
# 改进后的写法
def test_user_creation():
user = create_user("test@example.com")
assert user == {
"email": "test@example.com",
"is_active": True,
"groups": [],
}
改进后的版本虽然只有一行断言,但能提供更全面的验证。当测试失败时,Pytest会显示完整的差异对比,包括嵌套数据结构中的不匹配字段。
对于复杂对象的断言,我推荐使用pytest-assume插件进行多重断言:
python复制from pytest import assume
def test_complex_validation():
result = process_data()
with assume: assert result.status == "success"
with assume: assert len(result.items) > 0
with assume: assert result.timestamp is not None
传统assert在第一次失败后就停止,而pytest-assume会执行所有断言后再报告失败,极大提高了调试效率。
3.2 参数化测试进阶用法
基础参数化大家都懂,但实际项目中我们经常需要:
- 从外部文件加载测试数据
- 动态生成测试用例
- 参数化与fixture结合使用
场景1:CSV数据驱动测试
python复制import csv
import pytest
def load_test_data():
with open("tests/data/test_cases.csv") as f:
return list(csv.DictReader(f))
@pytest.mark.parametrize("data", load_test_data())
def test_with_external_data(data):
assert calculate(data["input"]) == data["expected"]
场景2:组合参数化
python复制@pytest.mark.parametrize("user_type", ["admin", "user", "guest"])
@pytest.mark.parametrize("page_size", [10, 25, 50])
def test_pagination(user_type, page_size):
result = get_list(user_type, size=page_size)
assert len(result) <= page_size
if user_type == "guest":
assert len(result) <= 10 # 访客限制
场景3:与fixture联动
python复制@pytest.fixture(params=["json", "xml", "yaml"])
def serializer(request):
return get_serializer(request.param)
@pytest.mark.parametrize("data", test_data)
def test_serialization(serializer, data):
assert data == serializer.loads(serializer.dumps(data))
3.3 Fixture深度应用
Fixture是Pytest最强大的功能之一,但90%的开发者只用了它10%的能力。来看几个高级用法:
自动清理资源
python复制@pytest.fixture
def temp_db():
db = Database()
db.start()
yield db # 测试执行阶段
db.stop() # 清理阶段
带参数的Fixture
python复制@pytest.fixture(params=[
("redis", 6379),
("memcached", 11211)
])
def cache_server(request):
server_type, port = request.param
server = start_server(server_type, port)
yield server
server.shutdown()
工厂模式Fixture
python复制@pytest.fixture
def user_factory():
users = []
def _factory(**kwargs):
user = User(**kwargs)
users.append(user)
return user
yield _factory
# 测试结束后清理所有创建的用户
for user in users:
user.delete()
在测试中使用:
python复制def test_user_roles(user_factory):
admin = user_factory(role="admin")
editor = user_factory(role="editor")
assert admin.can_edit_all_posts()
assert not editor.can_edit_all_posts()
4. 测试策略与最佳实践
4.1 单元测试设计原则
经过多年实践,我总结出了Pytest单元测试的"3-2-1"原则:
-
3种基本测试类型:
- 正向测试:验证正常输入的正确输出
- 边界测试:验证边界条件的处理
- 异常测试:验证错误输入的处理
-
2个关注点分离:
- 测试逻辑与测试数据分离
- 准备代码与断言代码分离
-
1个核心指标:每个测试用例只验证一个行为
示例:
python复制class TestShoppingCart:
# 正向测试
def test_add_item(self):
cart = ShoppingCart()
cart.add("apple", 1)
assert cart.get_count() == 1
# 边界测试
@pytest.mark.parametrize("qty", [0, 999, 1000])
def test_add_item_boundary(self, qty):
cart = ShoppingCart()
cart.add("apple", qty)
assert cart.get_count() == qty
# 异常测试
def test_add_negative_quantity(self):
cart = ShoppingCart()
with pytest.raises(ValueError):
cart.add("apple", -1)
4.2 集成测试技巧
集成测试最常见的痛点就是测试速度慢。通过合理使用fixture作用域可以显著提升速度:
python复制@pytest.fixture(scope="module")
def database():
# 整个测试模块只初始化一次数据库
db = Database()
db.start()
yield db
db.stop()
@pytest.fixture
def empty_db(database):
# 每个测试用例前清空数据
database.clear_all()
return database
另一个技巧是使用mock减少外部依赖:
python复制from unittest.mock import patch
def test_payment_integration():
with patch("payment.processor.charge") as mock_charge:
mock_charge.return_value = {"status": "success"}
result = make_payment(100)
assert result.is_success()
mock_charge.assert_called_once_with(100)
4.3 性能测试方案
虽然Pytest不是专门的性能测试工具,但结合pytest-benchmark插件可以做基本的性能回归测试:
python复制def test_search_performance(benchmark):
db = load_large_dataset() # 预加载大数据集
def setup():
return (db, "test_query"), {}
def teardown(db_query):
pass
result = benchmark.pedantic(
search_function,
setup=setup,
teardown=teardown,
rounds=100,
iterations=10
)
assert result < 0.1 # 100ms阈值
5. 高级特性与插件生态
5.1 常用插件推荐
经过上百个项目的验证,这些插件已经成为我的标配:
-
pytest-cov:代码覆盖率统计
bash复制
pytest --cov=src --cov-report=html -
pytest-xdist:并行测试
bash复制pytest -n auto # 自动检测CPU核心数 -
pytest-mock:更友好的mock支持
python复制def test_with_mock(mocker): mocker.patch("module.function", return_value=42) assert module.function() == 42 -
pytest-timeout:防止测试卡死
bash复制pytest --timeout=30 # 每个测试最多30秒 -
pytest-randomly:随机执行顺序发现依赖问题
bash复制pytest --randomly-dont-reorganize # 仅随机排序模块
5.2 自定义插件开发
当团队有特殊需求时,可以开发自己的Pytest插件。下面是一个简单的例子,用于跳过特定环境的测试:
python复制# conftest.py
def pytest_addoption(parser):
parser.addoption(
"--env",
action="store",
default="dev",
help="environment to run tests against"
)
def pytest_configure(config):
config.addinivalue_line(
"markers",
"env(name): mark test to run only on named environment"
)
def pytest_runtest_setup(item):
envmarker = item.get_closest_marker("env")
if envmarker is not None:
env_name = envmarker.args[0]
current_env = item.config.getoption("--env")
if current_env != env_name:
pytest.skip(f"test requires env {env_name}")
使用方式:
python复制@pytest.mark.env("prod")
def test_production_feature():
pass
5.3 测试报告优化
对于企业级项目,美观的测试报告至关重要。推荐以下组合:
bash复制pytest --html=report.html --self-contained-html
结合Allure框架可以生成更专业的报告:
bash复制pytest --alluredir=allure-results
allure serve allure-results
对于CI系统,JUnit格式报告是标配:
bash复制pytest --junitxml=report.xml
6. 常见问题排查
6.1 测试依赖问题
症状:测试单独运行通过,但一起运行时失败
解决方案:
- 检查是否有未清理的全局状态
- 使用
pytest-randomly发现隐式依赖 - 确保每个测试都使用独立的fixture实例
6.2 性能问题
症状:测试套件越来越慢
优化策略:
- 将
scope="function"改为scope="module"或scope="session" - 使用
pytest-xdist并行执行 - 用
--durations=10找出最慢的测试
6.3 间歇性失败
症状:测试有时通过有时失败
排查步骤:
- 检查是否有竞态条件
- 添加重试机制(谨慎使用)
bash复制
pytest --reruns 3 --reruns-delay 1 - 检查外部依赖的稳定性
6.4 数据库测试技巧
常见问题:测试污染数据库状态
解决方案模板:
python复制@pytest.fixture
def db_session():
session = create_session()
transaction = session.begin_nested()
yield session
transaction.rollback()
session.close()
这个fixture会在每个测试后回滚所有更改,保持数据库干净。
7. 企业级实践建议
7.1 CI/CD集成
在Jenkins中的典型配置:
groovy复制stage('Test') {
steps {
sh 'python -m pytest tests/ --junitxml=test-results.xml'
sh 'python -m pytest --cov=src/ --cov-report=xml:coverage.xml'
}
post {
always {
junit 'test-results.xml'
cobertura 'coverage.xml'
}
}
}
GitHub Actions示例:
yaml复制jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov
- name: Test with pytest
run: |
pytest --cov=./ --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v1
7.2 大型项目测试组织
对于包含多个子系统的项目,建议采用分层测试策略:
-
单元测试层:每个模块独立的测试,快速反馈
- 位置:
tests/unit/ - 特点:不涉及外部依赖,运行速度快
- 位置:
-
集成测试层:模块间交互测试
- 位置:
tests/integration/ - 特点:验证接口契约和数据流
- 位置:
-
系统测试层:完整业务流程测试
- 位置:
tests/system/ - 特点:使用真实环境配置
- 位置:
-
性能测试层:关键路径基准测试
- 位置:
tests/performance/ - 特点:单独执行,不纳入常规CI
- 位置:
7.3 测试代码维护
保持测试代码质量的建议:
- 定期重构:像对待生产代码一样重构测试代码
- 删除过时测试:移除不再相关的测试用例
- 避免过度mock:mock越多,测试价值越低
- 命名规范:
- 测试文件:
test_<module>.py - 测试函数:
test_<feature>_<scenario>() - fixture:
<resource>_fixture()
- 测试文件:
8. 实战案例:电商系统测试
让我们通过一个电商系统的典型场景展示Pytest的综合应用:
python复制# tests/integration/test_checkout.py
import pytest
@pytest.fixture
def empty_cart():
return ShoppingCart()
@pytest.fixture
def standard_user(user_factory):
return user_factory(credit=1000)
@pytest.fixture
def premium_user(user_factory):
return user_factory(credit=1000, is_premium=True)
@pytest.mark.parametrize("items,total", [
([("book", 20)], 20),
([("book", 20), ("pen", 5)], 25),
([("laptop", 999)], 999),
])
def test_checkout_normal_user(empty_cart, standard_user, items, total):
for name, price in items:
empty_cart.add(name, price)
order = checkout(empty_cart, standard_user)
assert order.total == total
assert standard_user.credit == 1000 - total
def test_checkout_premium_discount(empty_cart, premium_user):
empty_cart.add("book", 100)
order = checkout(empty_cart, premium_user)
assert order.total == 90 # 10%折扣
assert premium_user.credit == 1000 - 90
def test_checkout_insufficient_credit(empty_cart, standard_user):
empty_cart.add("car", 50000)
with pytest.raises(PaymentError):
checkout(empty_cart, standard_user)
这个测试套件展示了:
- fixture的层级复用
- 参数化测试不同场景
- 异常情况测试
- 业务规则验证(会员折扣)
9. 测试覆盖率优化
9.1 覆盖率分析
运行覆盖率报告:
bash复制pytest --cov=src --cov-report=term-missing
关键指标解读:
- 行覆盖率:最基础指标,但容易误导
- 分支覆盖率:更重要,确保所有条件分支都被测试
- 突变覆盖率:使用
pytest-mutagen等工具检测测试有效性
9.2 增量覆盖率策略
对于大型代码库,建议采用增量覆盖率检查:
bash复制pytest --cov=src --cov-report=term-missing --cov-fail-under=90 --no-cov-on-fail
在CI中配置:
yaml复制- name: Check coverage
run: |
pytest --cov=src --cov-report=xml
diff-cover coverage.xml --compare-branch=main --fail-under=90
9.3 难以覆盖的代码
处理难以测试的代码的策略:
- 第三方集成:使用contract测试验证接口而非实现
- UI组件:分离逻辑与渲染,单独测试逻辑部分
- 随机行为:固定随机种子进行测试
python复制def test_random_behavior(): random.seed(42) result1 = random_operation() random.seed(42) result2 = random_operation() assert result1 == result2
10. 测试数据管理
10.1 测试数据生成
使用factory_boy创建复杂测试数据:
python复制import factory
class UserFactory(factory.Factory):
class Meta:
model = User
username = factory.Faker("user_name")
email = factory.Faker("email")
is_active = True
@pytest.fixture
def test_users():
return UserFactory.create_batch(10)
10.2 敏感数据处理
永远不要在代码中硬编码敏感数据:
python复制# 错误做法
def test_payment():
process_payment("4111111111111111") # 测试信用卡号
# 正确做法
@pytest.fixture
def test_credit_card():
return os.getenv("TEST_CC_NUMBER", "4242424242424242") # Stripe测试号
10.3 数据库迁移测试
使用Alembic测试数据库迁移:
python复制from alembic.config import Config
from alembic import command
@pytest.fixture(scope="module")
def migrated_db():
alembic_cfg = Config("alembic.ini")
command.upgrade(alembic_cfg, "head")
yield
command.downgrade(alembic_cfg, "base")
def test_schema(migrated_db):
assert table_exists("users")
assert column_exists("users", "email")
11. 测试性能优化
11.1 测试并行化
使用pytest-xdist的最佳实践:
bash复制pytest -n auto --dist=loadfile # 按文件并行
对于有状态的服务,使用--dist=loadscope保持fixture作用域:
bash复制pytest -n 4 --dist=loadscope
11.2 测试选择策略
只运行修改相关的测试:
bash复制pytest --lf # 只运行上次失败的测试
pytest --ff # 先运行上次失败的测试
pytest -k "keyword" # 按名称筛选
11.3 耗时测试管理
标记慢测试并选择性运行:
python复制@pytest.mark.slow
def test_heavy_computation():
...
# 常规运行跳过慢测试
pytest -m "not slow"
# 专门运行慢测试
pytest -m slow
12. 测试报告与可视化
12.1 HTML报告增强
使用pytest-html的额外配置:
python复制# conftest.py
def pytest_html_report_title(report):
report.title = "项目测试报告"
def pytest_html_results_summary(prefix, summary, postfix):
prefix.extend([html.p("环境: " + os.environ.get("ENV", "dev"))])
12.2 Allure集成
生成丰富的交互式报告:
python复制import allure
@allure.feature("购物车")
class TestShoppingCart:
@allure.story("添加商品")
def test_add_item(self):
with allure.step("初始化购物车"):
cart = ShoppingCart()
with allure.step("添加商品"):
cart.add("apple", 1)
with allure.step("验证结果"):
assert cart.get_count() == 1
12.3 自定义报告钩子
添加团队特定指标:
python复制# conftest.py
def pytest_terminal_summary(terminalreporter, exitstatus, config):
duration = time.time() - terminalreporter._sessionstarttime
terminalreporter.write_sep(
"=",
f"测试总耗时: {duration:.2f}s | 平均每个测试: {duration/terminalreporter.stats['passed']:.3f}s",
blue=True
)
13. 测试安全实践
13.1 安全测试集成
使用bandit进行静态分析:
bash复制pytest --bandit
结合pytest-safety检查依赖漏洞:
bash复制pytest --safety
13.2 敏感信息防护
自动清理测试日志中的敏感信息:
python复制# conftest.py
@pytest.fixture(autouse=True)
def mask_secrets(caplog):
yield
for record in caplog.records:
if "password" in record.getMessage():
record.msg = record.msg.replace("password=secret", "password=****")
13.3 权限测试模式
验证最小权限原则:
python复制@pytest.mark.parametrize("role", ["guest", "user", "editor"])
def test_api_access(role):
client = ApiClient(role=role)
if role == "guest":
with pytest.raises(PermissionError):
client.delete_post(1)
else:
assert client.delete_post(1).status == "success"
14. 移动端与API测试
14.1 API契约测试
使用pytest-requests测试REST API:
python复制def test_api_contract(requests_mock):
requests_mock.get(
"https://api.example.com/users/1",
json={"id": 1, "name": "John"},
status_code=200
)
response = get_user(1)
assert response == {"id": 1, "name": "John"}
14.2 GraphQL测试
专用测试工具:
python复制from gql import gql
def test_graphql_query(schema):
query = gql("""
{
user(id: 1) {
name
email
}
}
""")
result = schema.execute(query)
assert result.errors is None
assert result.data["user"]["name"] == "John"
14.3 移动端测试集成
使用pytest-appium进行移动测试:
python复制@pytest.fixture
def mobile_driver():
capabilities = {
"platformName": "Android",
"deviceName": "emulator-5554",
"app": "/path/to/app.apk"
}
driver = webdriver.Remote("http://localhost:4723/wd/hub", capabilities)
yield driver
driver.quit()
def test_login(mobile_driver):
mobile_driver.find_element(By.ID, "username").send_keys("test")
mobile_driver.find_element(By.ID, "password").send_keys("pass")
mobile_driver.find_element(By.ID, "login").click()
assert mobile_driver.find_element(By.ID, "welcome").is_displayed()
15. 测试代码重构模式
15.1 重复测试消除
使用fixture提取重复逻辑:
python复制# 重构前
def test_admin_dashboard():
user = User(role="admin")
page = DashboardPage(user)
assert page.has_admin_tools()
def test_user_dashboard():
user = User(role="user")
page = DashboardPage(user)
assert not page.has_admin_tools()
# 重构后
@pytest.fixture(params=["admin", "user"])
def dashboard_page(request):
user = User(role=request.param)
return DashboardPage(user)
def test_dashboard_permissions(dashboard_page):
if dashboard_page.user.role == "admin":
assert dashboard_page.has_admin_tools()
else:
assert not dashboard_page.has_admin_tools()
15.2 复杂断言封装
将复杂验证逻辑提取为辅助函数:
python复制def assert_valid_user(user):
assert hasattr(user, "id")
assert isinstance(user.id, int)
assert "@" in user.email
assert 8 <= len(user.password_hash) <= 128
assert user.created_at <= datetime.now()
def test_user_creation():
user = create_user()
assert_valid_user(user)
15.3 测试数据生成器
创建可复用的数据生成器:
python复制# test_helpers/generators.py
def generate_product(**overrides):
defaults = {
"id": fake.uuid4(),
"name": fake.word(),
"price": fake.pydecimal(left_digits=2, right_digits=2, positive=True),
"stock": fake.pyint(min_value=0, max_value=100)
}
return {**defaults, **overrides}
# 测试中使用
def test_low_stock_alert():
product = generate_product(stock=2)
assert check_low_stock(product) is True
16. 测试驱动开发(TDD)实践
16.1 TDD工作流
- 编写失败的测试
python复制def test_add():
assert add(2, 3) == 5 # 函数还不存在
- 实现最小可通过版本
python复制def add(a, b):
return a + b
- 重构改进
python复制def add(*args):
return sum(args)
16.2 特性测试驱动
对于复杂功能,从高层测试开始:
python复制# 先编写功能测试
def test_checkout_flow():
cart = add_sample_items()
user = create_test_user()
order = checkout(cart, user)
assert order.total == 100
assert user.credit == 900
# 然后补充单元测试
def test_calculate_total():
items = [{"price": 10}, {"price": 20}]
assert calculate_total(items) == 30
16.3 测试维护策略
- 每日测试运行:纳入每日开发流程
- 失败即停止:CI中任何测试失败立即通知
- 测试所有权:谁写的代码谁负责维护测试
- 测试评审:代码评审必须包含测试评审
17. 遗留系统测试策略
17.1 测试覆盖策略
对于遗留代码,采用"测试边界"策略:
- 先为新修改的代码添加测试
- 然后为修复bug的代码添加测试
- 最后为高风险核心模块添加测试
17.2 测试解耦技巧
逐步解耦紧密耦合的代码:
python复制# 原始代码
def process_order(order):
db = Database.get_connection()
# 直接使用全局数据库连接
# 测试适配
def process_order(order, db=None):
db = db or Database.get_connection()
# 现在可以注入测试数据库
17.3 测试覆盖率提升
使用pytest-cov的增量模式:
bash复制pytest --cov=src --cov-append # 追加覆盖率数据
可视化未覆盖代码:
bash复制pytest --cov=src --cov-report=html
18. 测试文化构建
18.1 团队协作实践
- 测试代码标准:制定团队测试编写规范
- 测试知识库:积累常见测试模式和解决方案
- 测试日:定期进行测试重构和优化
18.2 质量指标可视化
使用pytest-benchmark的历史趋势:
bash复制pytest --benchmark-histogram=benchmark_results
构建质量仪表盘:
- 测试通过率
- 代码覆盖率趋势
- 测试执行时间变化
- 缺陷逃逸率
18.3 持续改进机制
- 测试回顾会议:分析测试有效性
- 缺陷根本分析:为什么测试没发现这个bug
- 测试技术分享:定期分享测试技巧
19. 性能测试进阶
19.1 基准测试策略
使用pytest-benchmark的正确姿势:
python复制def test_search_performance(benchmark):
db = load_large_dataset()
def setup():
return (db, "query"), {}
benchmark.pedantic(
search_function,
setup=setup,
rounds=10,
iterations=100
)
assert benchmark.stats["mean"] < 0.1
19.2 内存分析
使用pytest-memray检测内存问题:
bash复制pytest --memray tests/test_memory.py
分析内存泄漏:
python复制def test_memory_leak():
with MemoryMonitor() as monitor:
run_operation()
assert monitor.memory_delta < 1024 # 不超过1KB
19.3 I/O性能测试
模拟不同磁盘速度:
python复制@pytest.mark.parametrize("disk_speed", ["hdd", "ssd", "nvme"])
def test_disk_io(disk_speed, monkeypatch):
if disk_speed == "hdd":
monkeypatch.setattr("os.DISK_SPEED", 100)
elif disk_speed == "ssd":
monkeypatch.setattr("os.DISK_SPEED", 500)
result = disk_intensive_operation()
assert result < expected_time[disk_speed]
20. 测试框架扩展
20.1 自定义标记
创建有语义的标记:
python复制# conftest.py
def pytest_configure(config):
config.addinivalue_line(
"markers",
"smoke: 冒烟测试 - 快速验证核心功能"
)
@pytest.mark.smoke
def test_login():
assert login("test", "pass")
20.2 自定义命令行选项
添加团队特定选项:
python复制# conftest.py
def pytest_addoption(parser):
parser.addoption(
"--runslow",
action="store_true",
default=False,
help="运行慢速测试"
)
def pytest_collection_modifyitems(config, items):
if not config.getoption("--runslow"):
skip_slow = pytest.mark.skip(reason="需要--runslow选项")
for item in items:
if "slow" in item.keywords:
item.add_marker(skip_slow)
20.3 插件开发模式
创建简单插件示例:
python复制# pytest_teamplugin.py
def pytest_runtest_logstart(nodeid, location):
print(f"🚀 开始测试: {nodeid}")
def pytest_runtest_logfinish(nodeid, location):
print(f"✅ 完成测试: {nodeid}")
def pytest_runtest_logreport(report):
if report.failed:
print(f"❌ 失败: {report.nodeid}")
安装使用:
bash复制pip install -e .
pytest -p pytest_teamplugin
21. 多语言项目测试
21.1 国际化测试
验证多语言支持:
python复制@pytest.mark.parametrize("lang", ["en", "zh", "ja"])
def test_localization(lang):
set_locale(lang)
assert get_text("welcome") == expected[lang]
21.2 编码处理
测试不同编码的文件处理:
python复制@pytest.mark.parametrize("encoding", ["utf-8", "gbk", "shift-jis"])
def test_file_encoding(tmp_path, encoding):
file = tmp_path / "test.txt"
content = "测试内容"
file.write_text(content, encoding=encoding)
assert file.read_text(encoding=encoding) == content
21.3 时区测试
确保时区正确处理:
python复制@pytest.mark.parametrize("tz", ["UTC", "Asia/Shanghai", "America/New_York"])
def test_timezone_conversion(tz):
with set_timezone(tz):
now = datetime.now()
assert now.tzinfo.zone == tz
