python自动化测试:pytest(二)
2. 参数化测试(数据驱动)
Pytest 参数化与数据驱动测试详解
1. 基本参数化使用
@pytest.mark.parametrize 装饰器
import pytest
# 基本参数化示例
@pytest.mark.parametrize("input, expected", [
(1, 2),
(2, 4),
(3, 6)
])
def test_multiply_by_two(input, expected):
assert input * 2 == expected
# 多个参数示例
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3),
(5, 3, 8),
(10, -2, 8)
])
def test_addition(a, b, expected):
assert a + b == expected
2. 参数组合测试
import pytest
# 参数组合(笛卡尔积)
@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_combinations(x, y):
print(f"Testing with x={x}, y={y}")
# 测试逻辑
# 等价于测试以下组合:
# (0, 2), (0, 3), (1, 2), (1, 3)
3. 从函数动态生成参数
import pytest
def generate_test_data():
"""动态生成测试数据"""
return [
(i, i * 2) for i in range(5)
]
@pytest.mark.parametrize("input, expected", generate_test_data())
def test_dynamic_data(input, expected):
assert input * 2 == expected
4. 从外部文件读取数据
JSON 文件数据驱动
// test_data.json
[
{"input": 1, "expected": 2},
{"input": 5, "expected": 10},
{"input": 10, "expected": 20}
]
import pytest
import json
def load_test_data():
with open('test_data.json', 'r') as f:
return json.load(f)
@pytest.mark.parametrize("data", load_test_data())
def test_with_json_data(data):
assert data["input"] * 2 == data["expected"]
CSV 文件数据驱动
import pytest
import csv
def load_csv_data():
test_data = []
with open('test_data.csv', 'r') as f:
reader = csv.DictReader(f)
for row in reader:
test_data.append((
int(row['input']),
int(row['expected'])
))
return test_data
@pytest.mark.parametrize("input, expected", load_csv_data())
def test_with_csv_data(input, expected):
assert input * 2 == expected
5. IDs 参数化测试命名
import pytest
def id_func(val):
"""ids 函数会分别接收每个参数的值"""
if isinstance(val, int):
return f"input_{val}"
elif isinstance(val, str):
return f"string_{val}"
return str(val)
@pytest.mark.parametrize(
"input, expected",
[
(1, 2),
(5, 10),
(10, 20)
],
ids=id_func, # 这里不是接受元组,而是每一个元素
)
def test_with_custom_ids(input, expected):
assert input * 2 == expected
6. 类级别的参数化
import pytest
@pytest.mark.parametrize("input, expected", [
(1, 2),
(5, 10)
])
class TestMathOperations:
"""类级别的参数化 - 所有方法都会参数化"""
def test_multiply(self, input, expected):
assert input * 2 == expected
def test_division(self, input, expected):
assert expected / 2 == input
7. 间接参数化
import pytest
# 用于设置测试环境的fixture
@pytest.fixture
def setup_data(request):
"""根据参数设置不同的测试数据"""
return request.param * 2
@pytest.mark.parametrize("setup_data, expected", [
(1, 2),
(5, 10)
], indirect=["setup_data"]) # setup_data参数通过fixture处理
def test_with_indirect_param(setup_data, expected):
assert setup_data == expected
# 结构是setup,expected==》2, 2 和10,10
# 注意indirect可以放多个参数,但是要新建多个fixture,setup_data是上面的fixture的名字,注意fixture,parametrize,test_function的名字都是要统一的
8. 复杂数据结构的参数化
import pytest
@pytest.mark.parametrize("test_input, expected", [
# 字典参数
({"a": 1, "b": 2}, 3),
({"a": 5, "b": -3}, 2),
# 列表参数
([1, 2, 3], 6),
([10, 20, 30], 60),
# 元组参数
((1, 2), 3),
((5, -3), 2)
])
def test_complex_data_structures(test_input, expected):
if isinstance(test_input, dict):
result = test_input['a'] + test_input['b']
elif isinstance(test_input, (list, tuple)):
result = sum(test_input)
else:
result = test_input
assert result == expected
9. 条件参数化
import pytest
def should_run_test(condition):
"""根据条件决定是否运行测试"""
return condition == "run"
test_data = [
("run", 2, 4), # 运行
("skip", 3, 6), # 跳过
("run", 5, 10) # 运行
]
@pytest.mark.parametrize("condition, input, expected",
[case for case in test_data if should_run_test(case[0])]
)
def test_conditional_parameterization(condition, input, expected):
assert input * 2 == expected
10. 完整的实战示例
import pytest
import json
from typing import Dict, Any
# 从JSON文件加载测试数据
def load_user_test_cases():
with open('user_test_cases.json', 'r') as f:
return json.load(f)
# 用户验证测试类
class TestUserValidation:
@pytest.mark.parametrize("user_data, expected_result", [
(
{"username": "validuser", "email": "test@example.com", "age": 25},
{"valid": True, "errors": []}
),
(
{"username": "ab", "email": "invalid-email", "age": 15},
{"valid": False, "errors": ["用户名太短", "邮箱格式错误", "年龄不足"]}
)
])
def test_user_validation(self, user_data, expected_result):
# 实际的验证逻辑
is_valid, errors = self.validate_user(user_data)
assert is_valid == expected_result["valid"]
assert set(errors) == set(expected_result["errors"])
@pytest.mark.parametrize("test_case", load_user_test_cases())
def test_with_external_data(self, test_case):
"""使用外部数据文件进行测试"""
result = self.process_user_data(test_case["input"])
assert result == test_case["expected_output"]
def validate_user(self, user_data: Dict[str, Any]) -> tuple:
"""模拟用户验证逻辑"""
errors = []
# 用户名验证
if len(user_data.get("username", "")) < 3:
errors.append("用户名太短")
# 邮箱验证
if "@" not in user_data.get("email", ""):
errors.append("邮箱格式错误")
# 年龄验证
if user_data.get("age", 0) < 18:
errors.append("年龄不足")
return len(errors) == 0, errors
def process_user_data(self, input_data):
"""模拟数据处理逻辑"""
# 实际的数据处理代码
return {"processed": True, **input_data}
# 运行特定参数化测试的示例
if __name__ == "__main__":
# 只运行特定参数化的测试
pytest.main([
"-v",
"-k", "test_user_validation", # 只运行包含此名称的测试
"--tb=short" # 简短的错误回溯
])
11. 最佳实践和技巧
测试数据管理
import pytest
class TestData:
"""集中管理测试数据"""
VALID_CASES = [
(1, 1, 2),
(5, 3, 8),
(10, -5, 5)
]
INVALID_CASES = [
("a", 1, TypeError),
(1, "b", TypeError)
]
@pytest.mark.parametrize("a, b, expected", TestData.VALID_CASES)
def test_valid_addition(a, b, expected):
assert a + b == expected
@pytest.mark.parametrize("a, b, exception", TestData.INVALID_CASES)
def test_invalid_addition(a, b, exception):
with pytest.raises(exception):
a + b
动态跳过测试
import pytest
import sys
@pytest.mark.parametrize("input, expected", [
pytest.param(1, 2, id="normal_case"),
pytest.param(0, 0, id="zero_case"),
pytest.param(-1, -2, marks=pytest.mark.skip(reason="负数暂不支持"), id="negative_case"),
pytest.param(1000, 2000, marks=pytest.mark.skipif(
sys.version_info < (3, 8),
reason="需要Python 3.8+"
), id="large_number_case")
])
def test_with_skipping(input, expected):
assert input * 2 == expected
这些示例展示了 pytest 参数化和数据驱动测试的各种用法,可以根据实际测试需求选择合适的方法。
3. 标记和过滤测试
标记之前要配置pytest.ini
解决方法
方法1:在 pytest.ini 中注册标记(推荐)
创建 pytest.ini文件:
# pytest.ini
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
fast: marks tests as fast
smoke: smoke tests
integration: integration tests
regression: regression tests
windows: windows specific tests
linux: linux specific tests
方法2:在 pyproject.toml 中注册(新版本推荐)
# pyproject.toml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow",
"fast: marks tests as fast",
"smoke: smoke tests",
"integration: integration tests",
"regression: regression tests"
]
方法3:在 setup.cfg 中注册
# setup.cfg
[tool:pytest]
markers =
slow: marks tests as slow
fast: marks tests as fast
smoke: smoke tests
完整的项目结构示例
project/
├── pytest.ini # 配置文件
├── conftest.py # 可选:pytest配置
└── tests/
├── __init__.py
├── test_math.py
└── test_api.py
pytest.ini 内容示例:
# pytest.ini
[pytest]
markers =
slow: 标记运行缓慢的测试
fast: 标记运行快速的测试
smoke: 冒烟测试(核心功能)
integration: 集成测试
regression: 回归测试
database: 需要数据库的测试
api: API相关测试
ui: 用户界面测试
skip: 跳过测试
windows: 仅Windows平台
linux: 仅Linux平台
# 可选:其他配置
addopts = -v --tb=short
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
验证配置是否生效
创建测试文件验证:
# test_example.py
import pytest
import time
@pytest.mark.slow
def test_slow_operation():
"""这个测试应该可以正常运行,不会报错"""
time.sleep(0.1) # 模拟慢速操作
assert True
@pytest.mark.fast
def test_fast_operation():
"""快速测试"""
assert 1 + 1 == 2
@pytest.mark.smoke
def test_smoke():
"""冒烟测试"""
assert True
运行测试:
# 查看所有已注册的标记
pytest --markers
# 运行特定标记的测试
pytest -m slow -v
pytest -m "smoke or fast" -v
# 运行除慢速测试外的所有测试
pytest -m "not slow" -v
如果没有配置文件时的临时解决方案
如果暂时不想创建配置文件,可以使用以下方法之一:
方法A:使用 -p no:warnings忽略警告
pytest -p no:warnings -m slow
方法B:在 conftest.py 中动态注册
# conftest.py
def pytest_configure(config):
"""动态注册标记"""
config.addinivalue_line(
"markers", "slow: marks tests as slow"
)
config.addinivalue_line(
"markers", "fast: marks tests as fast"
)
config.addinivalue_line(
"markers", "smoke: smoke tests"
)
方法C:使用内置标记代替自定义标记
import pytest
import time
# 使用内置的skipif标记来模拟slow标记的功能
@pytest.mark.skipif(True, reason="慢速测试,默认跳过")
def test_slow_operation():
time.sleep(2)
assert True
# 运行时选择是否执行慢速测试
def test_slow_conditional():
if os.getenv('RUN_SLOW_TESTS'):
time.sleep(2)
assert True
最佳实践建议
- 总是创建 pytest.ini:即使是小项目也建议创建配置文件
- 详细描述标记:为每个标记提供清晰的描述
- 团队统一:确保团队使用相同的标记体系
- 定期清理:删除不再使用的标记
# 完整的 pytest.ini 示例
[pytest]
markers =
# 速度相关
slow: 运行时间超过1秒的测试
fast: 运行时间少于100ms的测试
# 测试类型
unit: 单元测试
integration: 集成测试
e2e: 端到端测试
smoke: 核心功能冒烟测试
regression: 回归测试
# 功能模块
api: API相关测试
database: 数据库相关测试
auth: 认证授权测试
ui: 用户界面测试
# 环境相关
windows: 仅Windows平台
linux: 仅Linux平台
macos: 仅macOS平台
# 其他配置
addopts = -ra -q --strict-markers
filterwarnings =
ignore:.*deprecated.*:DeprecationWarning
创建好配置文件后,你的 @pytest.mark.slow就不会再报错了!
pytest 提供了丰富的标记(mark)和过滤机制,让我详细介绍各种用法:
1. 基本标记语法
定义和使用标记
import pytest
# 标记单个测试函数
@pytest.mark.slow
def test_slow_operation():
import time
time.sleep(2)
assert True
# 标记测试类
@pytest.mark.integration
class TestIntegrationSuite:
def test_database(self):
assert True
def test_api(self):
assert True
# 多个标记
@pytest.mark.slow
@pytest.mark.integration
def test_slow_integration():
assert True
2. 自定义标记注册
在 pytest.ini 中注册标记
# pytest.ini
[pytest]
markers =
slow: 标记运行缓慢的测试
integration: 集成测试
smoke: 冒烟测试
regression: 回归测试
windows: 仅Windows平台运行
linux: 仅Linux平台运行
skip: 跳过测试
xfail: 预期失败的测试
3. 内置标记
跳过测试
import pytest
import sys
# 无条件跳过
@pytest.mark.skip(reason="功能尚未实现")
def test_unimplemented():
assert False
# 条件跳过
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8+")
def test_python38_feature():
assert True
# 跳过整个模块
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="不在Windows上运行")
@pytest.mark.skipif(not hasattr(sys, 'getwindowsversion'), reason="仅Windows平台")
def test_windows_specific():
assert True
预期失败标记
import pytest
@pytest.mark.xfail(reason="已知问题,正在修复")
def test_buggy_feature():
assert False # 预期会失败
@pytest.mark.xfail(run=False, reason="太耗时,不运行")
def test_very_slow():
import time
time.sleep(10)
assert True
# 严格模式:如果测试通过则报错
@pytest.mark.xfail(strict=True, reason="应该失败但通过了")
def test_should_fail_but_passes():
assert True # 这会报错,因为预期失败但实际通过了
4. 参数化标记
import pytest
# 为参数化测试的特定用例添加标记
@pytest.mark.parametrize(
"input,expected",
[
pytest.param(1, 2, marks=pytest.mark.normal),
pytest.param(5, 10, marks=pytest.mark.fast),
pytest.param(10, 20, marks=pytest.mark.slow),
pytest.param(-1, -2, marks=pytest.mark.xfail(reason="负数不支持")),
]
)
def test_parameterized_with_marks(input, expected):
assert input * 2 == expected
5. 自定义标记与过滤
运行特定标记的测试
# 运行所有slow标记的测试
pytest -m slow
# 运行integration或smoke标记的测试
pytest -m "integration or smoke"
# 运行slow但不是integration的测试
pytest -m "slow and not integration"
# 运行smoke和regression的测试
pytest -m "smoke and regression"
复杂逻辑组合
import pytest
@pytest.mark.smoke
@pytest.mark.regression
class TestCriticalFunctionality:
@pytest.mark.fast
def test_quick_check(self):
assert True
@pytest.mark.slow
@pytest.mark.integration
def test_comprehensive(self):
assert True
运行命令:
# 运行同时有smoke和regression标记的测试
pytest -m "smoke and regression"
# 运行有smoke标记,但没有slow标记的测试
pytest -m "smoke and not slow"
6. 基于测试名称的过滤
# test_math_operations.py
def test_addition():
assert 1 + 1 == 2
def test_subtraction():
assert 5 - 3 == 2
def test_multiplication():
assert 2 * 3 == 6
class TestAdvancedMath:
def test_division(self):
assert 6 / 2 == 3
def test_power(self):
assert 2 ** 3 == 8
运行命令:
# 运行名称包含"add"的测试
pytest -k "add"
# 运行名称包含"TestAdvancedMath"的测试
pytest -k "TestAdvancedMath"
# 运行名称包含"test"但不包含"Advanced"的测试
pytest -k "test and not Advanced"
# 使用正则表达式
pytest -k "test_.*ion" # 匹配test_addition, test_subtraction等
7. 基于目录和文件的过滤
# 运行特定目录下的测试
pytest tests/integration/
# 运行特定文件中的测试
pytest tests/unit/test_math.py
# 运行特定文件中的特定测试类
pytest tests/unit/test_math.py::TestAdvancedMath
# 运行特定测试方法
pytest tests/unit/test_math.py::TestAdvancedMath::test_division
8. 复杂过滤策略
结合多种过滤方式
# 多条件组合过滤
pytest -m "smoke and not slow" -k "api" tests/integration/
# 运行特定标记且名称包含特定字符串的测试
pytest -m regression -k "user" --tb=short
# 使用配置文件定义默认过滤
9. 动态标记
import pytest
import os
def pytest_runtest_setup(item):
"""动态为测试添加标记"""
# 根据环境变量动态标记
if os.getenv('TEST_ENV') == 'CI':
item.add_marker(pytest.mark.ci)
# 根据测试名称动态标记
if 'integration' in item.nodeid:
item.add_marker(pytest.mark.integration)
# 使用自定义标记装饰器
def conditional_mark(condition, mark):
"""条件性标记装饰器"""
def decorator(func):
if condition:
return pytest.mark.__getattr__(mark)(func)
return func
return decorator
# 使用示例
@conditional_mark(os.getenv('ENVIRONMENT') == 'production', 'production')
def test_production_only():
assert True
10. 标记的作用域和继承
import pytest
# 模块级别标记
pytestmark = pytest.mark.module_marker
# 类级别标记 - 会被所有方法继承
@pytest.mark.class_marker
class TestExample:
# 这个方法会有 module_marker 和 class_marker
def test_inherits_marks(self):
assert True
# 这个方法会有 module_marker, class_marker 和 method_marker
@pytest.mark.method_marker
def test_with_additional_mark(self):
assert True
# 覆盖类级别标记,这里说的是覆盖了上面的类mark
@pytest.mark.new_class_marker
class TestOverride:
@pytest.mark.override_marker
def test_override(self):
assert True
11. 实际项目中的标记策略
示例:完整的测试标记体系
# conftest.py
import pytest
import os
def pytest_configure(config):
"""配置自定义标记"""
config.addinivalue_line(
"markers", "smoke: 核心功能冒烟测试"
)
config.addinivalue_line(
"markers", "integration: 集成测试"
)
config.addinivalue_line(
"markers", "slow: 运行缓慢的测试"
)
config.addinivalue_line(
"markers", "database: 需要数据库的测试"
)
# test_suite.py
import pytest
@pytest.mark.smoke
class TestCoreFeatures:
@pytest.mark.fast
def test_user_login(self):
assert True
@pytest.mark.database
def test_user_profile(self):
assert True
@pytest.mark.integration
@pytest.mark.slow
class TestIntegration:
@pytest.mark.database
def test_order_workflow(self):
assert True
def test_payment_integration(self):
assert True
@pytest.mark.regression
class TestBugFixes:
def test_fixed_issue_123(self):
assert True
常用运行命令示例
# 日常开发:快速运行核心测试
pytest -m "smoke and not slow" -v
# CI流水线:运行所有非慢速测试
pytest -m "not slow" --tb=short
# 集成测试环境:运行集成测试
pytest -m integration -x # 遇到失败立即停止
# 回归测试:运行所有回归测试
pytest -m regression --maxfail=5 # 最多允许5个失败
# 按目录和标记组合过滤
pytest tests/api/ -m "not database" -k "test_get"
# 生成测试报告
pytest -m smoke --junitxml=report.xml --html=report.html
12. 高级过滤技巧
使用pytest钩子进行自定义过滤
# conftest.py
def pytest_collection_modifyitems(config, items):
"""自定义测试收集逻辑"""
# 如果设置了ONLY_CRITICAL环境变量,只运行smoke测试
if os.getenv('ONLY_CRITICAL'):
selected = []
deselected = []
for item in items:
if item.get_closest_marker('smoke'):
selected.append(item)
else:
deselected.append(item)
config.hook.pytest_deselected(items=deselected)
items[:] = selected
# 根据平台过滤
if sys.platform == "win32":
for item in items[:]:
if item.get_closest_marker('linux_only'):
items.remove(item)
这些标记和过滤机制让 pytest 具有极强的灵活性,可以根据不同场景运行不同的测试集合。
mark这个东西不会被覆盖,只会累加
四、阶段3:高级特性(5-7天)
1. conftest.py 共享 Fixture
conftest.py是 pytest 的核心特性之一,用于在测试目录结构中共享 fixture 和配置。让我详细解释它的用法:
1. conftest.py 的基本概念
文件位置和作用域
project/
├── conftest.py # 项目根目录,对所有测试可见
├── tests/
│ ├── conftest.py # tests目录,对tests/下所有测试可见
│ ├── unit/
│ │ ├── conftest.py # unit目录,对unit/下测试可见
│ │ ├── test_math.py
│ │ └── test_strings.py
│ ├── integration/
│ │ ├── conftest.py # integration目录,对integration/下测试可见
│ │ ├── test_api.py
│ │ └── test_database.py
│ └── e2e/
│ ├── conftest.py # e2e目录,对e2e/下测试可见
│ └── test_workflows.py
└── src/
2. 基本 Fixture 共享
项目级共享 fixture
# conftest.py (项目根目录)
import pytest
import requests
from datetime import datetime
@pytest.fixture
def api_base_url():
"""API基础URL - 所有测试都可以使用"""
return "https://api.example.com"
@pytest.fixture
def timestamp():
"""当前时间戳"""
return datetime.now().isoformat()
@pytest.fixture
def default_headers():
"""默认请求头"""
return {
"User-Agent": "pytest-tests",
"Content-Type": "application/json"
}
3. 作用域不同的 conftest.py
项目级 conftest.py
# conftest.py (项目根目录)
import pytest
import os
@pytest.fixture(scope="session")
def database_url():
"""会话级别的数据库URL"""
return os.getenv("DATABASE_URL", "sqlite:///:memory:")
@pytest.fixture
def app_config():
"""应用配置"""
return {
"debug": True,
"testing": True,
"secret_key": "test-secret-key"
}
测试目录级 conftest.py
# tests/conftest.py
import pytest
@pytest.fixture
def test_client():
"""测试客户端 - 只在tests目录下可用"""
from myapp import create_app
app = create_app()
app.config['TESTING'] = True
return app.test_client()
@pytest.fixture
def authenticated_client(test_client):
"""已认证的测试客户端"""
# 使用test_client fixture,自动处理依赖
test_client.post('/login', json={'username': 'test', 'password': 'test'})
return test_client
子目录级 conftest.py
# tests/unit/conftest.py
import pytest
@pytest.fixture
def mock_database():
"""单元测试专用的模拟数据库"""
from unittest.mock import Mock
mock_db = Mock()
mock_db.query.return_value.all.return_value = []
return mock_db
@pytest.fixture
def sample_user_data():
"""用户测试数据"""
return {
"username": "testuser",
"email": "test@example.com",
"age": 25
}
# tests/integration/conftest.py
import pytest
import tempfile
import os
@pytest.fixture(scope="module")
def temp_database():
"""集成测试用的临时数据库"""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# 设置测试数据库
yield f"sqlite:///{db_path}"
# 清理
if os.path.exists(db_path):
os.unlink(db_path)
@pytest.fixture
def api_client(temp_database):
"""API测试客户端"""
from myapp import create_app
app = create_app()
app.config['SQLALCHEMY_DATABASE_URI'] = temp_database
with app.test_client() as client:
yield client
4. 实际测试用例使用
单元测试使用 fixtures
# tests/unit/test_math.py
def test_addition():
assert 1 + 1 == 2
def test_user_creation(sample_user_data, mock_database):
"""使用unit/conftest.py中的fixtures"""
from myapp import User
user = User(**sample_user_data)
mock_database.add(user)
mock_database.commit.assert_called_once()
# tests/unit/test_strings.py
def test_string_operations():
assert "hello".upper() == "HELLO"
集成测试使用 fixtures
# tests/integration/test_api.py
def test_api_endpoint(api_client, authenticated_client):
"""使用integration/conftest.py中的fixtures"""
# 测试未认证访问
response = api_client.get('/api/protected')
assert response.status_code == 401
# 测试认证访问
response = authenticated_client.get('/api/protected')
assert response.status_code == 200
def test_database_integration(temp_database):
"""测试数据库集成"""
from myapp.models import init_db
db = init_db(temp_database)
# 数据库操作测试
端到端测试
# tests/e2e/test_workflows.py
def test_complete_workflow(test_client, api_base_url):
"""使用项目级fixtures测试完整流程"""
# 完整的端到端测试
response = test_client.post('/api/orders', json={
'items': ['product1', 'product2']
})
assert response.status_code == 201
5. Fixture 依赖和组合
复杂的 Fixture 依赖链
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def database_engine():
"""数据库引擎"""
from sqlalchemy import create_engine
engine = create_engine("sqlite:///:memory:")
return engine
@pytest.fixture
def database_session(database_engine):
"""数据库会话"""
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=database_engine)
session = Session()
yield session
session.close()
@pytest.fixture
def user_factory(database_session):
"""用户工厂"""
def create_user(username, email):
from myapp.models import User
user = User(username=username, email=email)
database_session.add(user)
database_session.commit()
return user
return create_user
@pytest.fixture
def sample_user(user_factory):
"""样例用户"""
return user_factory("testuser", "test@example.com")
6. 自动使用 Fixture (autouse)
# tests/conftest.py
import pytest
@pytest.fixture(autouse=True)
def setup_test_environment():
"""自动为每个测试设置环境"""
print("\n=== 设置测试环境 ===")
# 设置代码
yield
print("=== 清理测试环境 ===")
# 清理代码
@pytest.fixture(autouse=True, scope="session")
def setup_logging():
"""会话级别的日志设置"""
import logging
logging.basicConfig(level=logging.INFO)
print("=== 设置日志 ===")
7. 参数化 Fixture
# tests/conftest.py
import pytest
@pytest.fixture(params=["mysql", "postgresql", "sqlite"])
def database_type(request):
"""参数化的数据库类型fixture"""
return request.param
@pytest.fixture
def database_url(database_type):
"""根据数据库类型返回URL"""
urls = {
"mysql": "mysql://user:pass@localhost/test",
"postgresql": "postgresql://user:pass@localhost/test",
"sqlite": "sqlite:///:memory:"
}
return urls[database_type]
# 使用参数化fixture的测试
def test_database_compatibility(database_url, database_type):
print(f"Testing {database_type} with {database_url}")
# 测试逻辑
8. 动态 Fixture
# tests/conftest.py
import pytest
def create_user_fixture(role, permissions):
"""动态创建用户fixture的工厂函数"""
@pytest.fixture
def user_fixture():
return {
"username": f"test_{role}",
"role": role,
"permissions": permissions
}
return user_fixture
# 创建不同的用户fixture
admin_user = create_user_fixture("admin", ["read", "write", "delete"])
editor_user = create_user_fixture("editor", ["read", "write"])
viewer_user = create_user_fixture("viewer", ["read"])
# 使这些fixture在conftest.py中可用
@pytest.fixture
def admin_user_fixture():
return admin_user()
@pytest.fixture
def editor_user_fixture():
return editor_user()
@pytest.fixture
def viewer_user_fixture():
return viewer_user()
9. 完整的实战示例
项目结构
myproject/
├── conftest.py
├── tests/
│ ├── conftest.py
│ ├── unit/
│ │ ├── conftest.py
│ │ ├── test_models.py
│ │ └── test_services.py
│ ├── integration/
│ │ ├── conftest.py
│ │ ├── test_api.py
│ │ └── test_database.py
│ └── e2e/
│ ├── conftest.py
│ └── test_workflows.py
└── src/
根目录 conftest.py
# conftest.py
import pytest
import os
from datetime import datetime
@pytest.fixture(scope="session")
def project_root():
"""项目根目录路径"""
return os.path.dirname(os.path.abspath(__file__))
@pytest.fixture(scope="session")
def test_start_time():
"""测试开始时间"""
return datetime.now()
@pytest.fixture(autouse=True)
def print_test_info(request, test_start_time):
"""自动打印测试信息"""
test_name = request.node.name
print(f"\n🚀 开始测试: {test_name}")
yield
duration = (datetime.now() - test_start_time).total_seconds()
print(f"✅ 测试完成: {test_name} (耗时: {duration:.2f}s)")
tests/conftest.py
# tests/conftest.py
import pytest
import tempfile
import json
@pytest.fixture
def temp_config_file():
"""临时配置文件"""
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
config = {
"database": "sqlite:///:memory:",
"debug": True,
"secret_key": "test-secret"
}
json.dump(config, f)
temp_path = f.name
yield temp_path
# 清理
import os
os.unlink(temp_path)
@pytest.fixture
def app(temp_config_file):
"""应用实例"""
from myapp import create_app
return create_app(config_file=temp_config_file)
@pytest.fixture
def client(app):
"""测试客户端"""
return app.test_client()
tests/unit/conftest.py
# tests/unit/conftest.py
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_database():
"""模拟数据库"""
db = Mock()
db.session = Mock()
return db
@pytest.fixture
def sample_data():
"""样例数据"""
return {
"users": [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
],
"products": [
{"id": 1, "name": "Product A", "price": 100},
{"id": 2, "name": "Product B", "price": 200}
]
}
测试用例示例
# tests/unit/test_models.py
def test_user_model(mock_database, sample_data):
"""使用unit级别的fixtures"""
from myapp.models import User
user_data = sample_data["users"][0]
user = User(**user_data)
mock_database.session.add(user)
mock_database.session.commit.assert_called_once()
# tests/integration/test_api.py
def test_api_integration(client, temp_config_file):
"""使用integration级别的fixtures"""
response = client.get('/api/users')
assert response.status_code == 200
10. 最佳实践
- 按作用域组织:将fixture放在合适作用域的conftest.py中
- 避免循环依赖:注意fixture之间的依赖关系
- 使用适当的scope:session > module > class > function
- 命名清晰:fixture名称要能清晰表达其用途
- 文档完善:为每个fixture添加docstring
这样组织conftest.py可以让你的测试代码更加模块化和可维护!
2. 插件系统使用
这些是 pytest 最常用的插件,每个都有特定的用途。让我详细介绍它们的作用和用法:
1. pytest-html - 生成HTML测试报告
安装和基本使用
pip install pytest-html
pytest --html=report.html
配置和定制
# pytest.ini
[pytest]
addopts = --html=reports/report.html --self-contained-html
# 生成带时间戳的报告
pytest --html=reports/report-{datetime}.html
# 添加环境信息
pytest --html=report.html --title="我的测试报告" --self-contained-html
在代码中自定义报告
# conftest.py
import pytest
from py.xml import html
def pytest_html_results_table_header(cells):
"""自定义报告表头"""
cells.insert(2, html.th("Description"))
cells.insert(1, html.th("Time", class_="sortable time", col="time"))
def pytest_html_results_table_row(report, cells):
"""自定义报告行"""
cells.insert(2, html.td(report.description))
cells.insert(1, html.td(report.duration, class_="col-time"))
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""添加测试描述"""
outcome = yield
report = outcome.get_result()
report.description = str(item.function.__doc__)
2. pytest-sugar - 美化测试进度显示
安装和效果
pip install pytest-sugar
pytest # 自动使用美化输出
特性
- ✅ 进度条显示
- ✅ 彩色输出
- ✅ 即时显示进度(不用等到测试结束)
- ✅ 显示失败测试的即时反馈
禁用sugar(如果需要)
pytest -p no:sugar # 临时禁用
3. pytest-xdist - 并行测试
安装和基本使用
pip install pytest-xdist
# 使用所有CPU核心
pytest -n auto
# 指定进程数
pytest -n 4
# 每CPU一个进程
pytest -n logical
高级用法
# 分布式测试到其他机器
pytest -d --tx ssh=user@server1 --tx ssh=user@server2
# 指定测试分发方式
pytest -n 4 --dist=loadscope # 按模块分发
pytest -n 4 --dist=loadfile # 按文件分发
# 限制每个测试的执行时间
pytest -n 4 --timeout=300 # 5分钟超时
处理并行测试的问题
# conftest.py
import pytest
import tempfile
import os
@pytest.fixture(scope="session")
def session_temp_dir():
"""会话级临时目录,xdist安全"""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
@pytest.fixture
def worker_temp_file(request, session_temp_dir):
"""每个工作进程的临时文件"""
worker_id = getattr(request.config, 'workerinput', {}).get('workerid', 'master')
temp_file = os.path.join(session_temp_dir, f"temp_{worker_id}.txt")
with open(temp_file, 'w') as f:
f.write(f"Worker {worker_id} data")
yield temp_file
# 清理
if os.path.exists(temp_file):
os.unlink(temp_file)
4. pytest-marks - 标记管理(实际是内置功能)
# 查看所有标记
pytest --markers
# 标记使用示例
pytest -m "slow and not integration"
5. pytest-django - Django项目测试
安装和配置
pip install pytest-django
# 创建pytest.ini
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = tests.py test_*.py *_tests.py
addopts = --reuse-db
常用fixtures和功能
# conftest.py
import pytest
@pytest.fixture
def admin_user(django_user_model):
"""创建管理员用户"""
return django_user_model.objects.create_superuser(
username='admin',
email='admin@example.com',
password='password'
)
@pytest.fixture
def authenticated_client(client, admin_user):
"""已认证的客户端"""
client.force_login(admin_user)
return client
# test_views.py
import pytest
from django.urls import reverse
@pytest.mark.django_db
def test_homepage(client):
"""测试首页"""
response = client.get(reverse('home'))
assert response.status_code == 200
def test_admin_view(authenticated_client):
"""测试需要认证的视图"""
response = authenticated_client.get('/admin/')
assert response.status_code == 200
@pytest.mark.django_db
class TestUserViews:
def test_user_list(self, client, admin_user):
response = client.get('/users/')
assert response.status_code == 200
def test_user_create(self, authenticated_client):
response = authenticated_client.post('/users/create/', {
'username': 'newuser',
'email': 'new@example.com'
})
assert response.status_code == 302 # 重定向
数据库管理
# 重用测试数据库(加速测试)
pytest --reuse-db
# 创建测试数据库
pytest --create-db
# 并行测试时的数据库设置
pytest -n auto --reuse-db
6. pytest-flask - Flask项目测试
安装和配置
pip install pytest-flask
基本用法
# conftest.py
import pytest
from myapp import create_app
@pytest.fixture
def app():
"""Flask应用fixture"""
app = create_app()
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False # 禁用CSRF用于测试
return app
# test_views.py
import pytest
import json
def test_index(client):
"""测试首页"""
response = client.get('/')
assert response.status_code == 200
assert b'Welcome' in response.data
def test_api_endpoint(client):
"""测试API端点"""
response = client.get('/api/data')
assert response.status_code == 200
data = json.loads(response.data)
assert 'result' in data
def test_post_request(client):
"""测试POST请求"""
response = client.post('/api/users', json={
'username': 'testuser',
'email': 'test@example.com'
})
assert response.status_code == 201
@pytest.fixture
def authenticated_client(client, auth_header):
"""已认证的客户端"""
client.environ_base['HTTP_AUTHORIZATION'] = auth_header
return client
def test_protected_endpoint(authenticated_client):
"""测试需要认证的端点"""
response = authenticated_client.get('/api/protected')
assert response.status_code == 200
7. 插件组合使用示例
完整的测试命令示例
# 并行运行测试,生成HTML报告,使用美化输出
pytest -n auto --html=report.html --self-contained-html -v
# Django项目:并行测试+HTML报告+数据库重用
pytest -n 4 --html=reports/test_report.html --reuse-db -m "not slow"
# Flask项目:特定标记测试
pytest -m "api and not slow" --html=api_test_report.html
配置文件示例
# pytest.ini
[pytest]
addopts = -v --strict-markers --html=reports/report.html -n auto
markers =
slow: marks tests as slow
fast: marks tests as fast
api: API related tests
integration: integration tests
smoke: smoke tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# Django特定
DJANGO_SETTINGS_MODULE = myproject.settings
# 或者Flask特定
flask_app = myapp:create_app()
在CI/CD中使用的示例
# .github/workflows/test.yml
name: 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
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-html pytest-xdist pytest-django
- name: Run tests
run: |
pytest -n auto --html=report.html --self-contained-html --junitxml=report.xml
- name: Upload test results
uses: actions/upload-artifact@v2
with:
name: test-report
path: report.html
8. 其他有用的插件
# 测试覆盖率
pip install pytest-cov
pytest --cov=myapp --cov-report=html
# 随机化测试顺序
pip install pytest-randomly
pytest --randomly-dont-reorganize # 保持模块顺序
# 异步测试支持
pip install pytest-asyncio
# Mock工具集成
pip install pytest-mock
# 基准测试
pip install pytest-benchmark
# 测试排序控制
pip install pytest-ordering
这些插件的组合使用可以大幅提升测试效率和质量,特别是在大型项目中。
3. 钩子函数(Hooks)
pytest 的钩子函数(hook functions)是强大的扩展机制,允许你在测试过程的不同阶段插入自定义逻辑。让我详细解释它们的作用和用法:
1. 钩子函数的基本概念
什么是钩子函数
钩子函数是 pytest 在测试执行的特定阶段调用的函数,允许你:
修改测试行为
添加额外功能
收集自定义信息
控制测试流程
钩子函数的放置位置
# conftest.py - 钩子函数通常放在这里
import pytest
import sys
def pytest_configure(config):
"""在配置阶段调用"""
print("=== pytest配置开始 ===")
def pytest_unconfigure(config):
"""在退出阶段调用"""
print("=== pytest退出 ===")
2. 测试收集阶段的钩子
修改测试收集
# conftest.py
def pytest_collection_modifyitems(config, items):
"""
修改收集到的测试项
config: pytest配置对象
items: 收集到的测试项列表
"""
print(f"收集到 {len(items)} 个测试")
# 示例1: 按测试名称排序
items.sort(key=lambda item: item.nodeid)
# 示例2: 跳过名称包含"slow"的测试
if config.getoption("--skip-slow"):
skip_slow = pytest.mark.skip(reason="跳过慢速测试")
for item in items:
if "slow" in item.nodeid:
item.add_marker(skip_slow)
# 示例3: 为所有测试添加标记
for item in items:
if "integration" in item.nodeid:
item.add_marker(pytest.mark.integration)
def pytest_collect_file(file_path, path, parent):
"""决定是否收集某个文件"""
if file_path.suffix == ".yaml":
# 自定义收集YAML文件作为测试
return YamlFile.from_parent(parent, path=file_path)
3. 测试运行阶段的钩子
测试设置和清理
# conftest.py
def pytest_runtest_setup(item):
"""在每个测试运行前调用"""
print(f"准备运行: {item.nodeid}")
def pytest_runtest_teardown(item, nextitem):
"""在每个测试运行后调用"""
print(f"清理: {item.nodeid}")
def pytest_runtest_call(item):
"""在测试函数调用时执行"""
print(f"调用测试: {item.nodeid}")
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_protocol(item, nextitem):
"""
包装测试执行协议
可以完全控制测试执行流程
"""
print(f"开始测试协议: {item.nodeid}")
outcome = yield # 在这里实际执行测试
print(f"结束测试协议: {item.nodeid}")
4. 测试报告生成钩子
自定义测试报告
# conftest.py
def pytest_runtest_makereport(item, call):
"""
为每个测试生成报告
call: 包含测试调用信息(setup/call/teardown)
"""
if call.when == "call": # 只关注测试调用阶段
if call.excinfo is not None: # 测试失败
print(f"测试失败: {item.nodeid}")
print(f"错误: {call.excinfo.value}")
# 添加自定义属性到报告
setattr(item, "test_duration", call.stop - call.start)
def pytest_report_teststatus(report, config):
"""自定义测试状态报告"""
if report.when == "call":
if report.passed:
return "✅", "PASS", "绿色"
elif report.failed:
return "❌", "FAIL", "红色"
return None # 使用默认行为
5. 配置和初始化钩子
初始化和配置
# conftest.py
def pytest_configure(config):
"""pytest配置初始化时调用"""
# 添加自定义配置
config.addinivalue_line(
"markers", "slow: 标记慢速测试"
)
# 添加自定义命令行选项
config.addinivalue_line(
"addopts", "--strict-markers"
)
print("pytest配置完成")
def pytest_sessionstart(session):
"""测试会话开始时调用"""
print("=== 测试会话开始 ===")
# 初始化资源:数据库连接、API客户端等
def pytest_sessionfinish(session, exitstatus):
"""测试会话结束时调用"""
print(f"=== 测试会话结束,退出状态: {exitstatus} ===")
# 清理资源
def pytest_unconfigure(config):
"""pytest退出时调用"""
print("pytest退出清理")
6. 自定义命令行选项
添加命令行参数
# conftest.py
def pytest_addoption(parser):
"""添加自定义命令行选项"""
parser.addoption(
"--env",
action="store",
default="test",
help="测试环境: test, staging, production"
)
parser.addoption(
"--run-slow",
action="store_true",
default=False,
help="运行慢速测试"
)
parser.addoption(
"--browser",
action="store",
choices=["chrome", "firefox", "safari"],
default="chrome",
help="浏览器选择"
)
def pytest_configure(config):
"""使用命令行选项"""
env = config.getoption("--env")
print(f"测试环境: {env}")
# 根据环境设置不同的配置
if env == "production":
os.environ["DATABASE_URL"] = "prod_connection_string"
7. 插件管理钩子
插件加载和管理
# conftest.py
def pytest_plugin_registered(plugin, manager):
"""当插件注册时调用"""
print(f"插件已注册: {plugin}")
def pytest_addhooks(pluginmanager):
"""添加自定义钩子"""
# 注册自定义钩子
pluginmanager.add_hookspecs(MyHooks)
8. 完整的实战示例
示例1:自定义测试过滤和排序
# conftest.py
import pytest
import os
def pytest_collection_modifyitems(config, items):
"""自定义测试收集和排序"""
# 1. 根据环境变量过滤测试
if os.getenv("CI") == "true":
# CI环境中跳过慢速测试
skip_slow = pytest.mark.skip(reason="CI环境中跳过慢速测试")
for item in items:
if item.get_closest_marker("slow"):
item.add_marker(skip_slow)
# 2. 按优先级排序测试
def get_test_priority(item):
if item.get_closest_marker("smoke"):
return 1 # 冒烟测试优先
elif item.get_closest_marker("integration"):
return 3 # 集成测试次之
else:
return 2 # 普通单元测试
items.sort(key=get_test_priority)
# 3. 为特定测试添加超时标记
for item in items:
if "api" in item.nodeid:
item.add_marker(pytest.mark.timeout(30))
def pytest_runtest_setup(item):
"""测试设置"""
# 记录测试开始时间
item.start_time = pytest.time.time()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""生成自定义测试报告"""
outcome = yield
report = outcome.get_result()
# 计算测试耗时
if hasattr(item, 'start_time'):
duration = pytest.time.time() - item.start_time
report.duration = duration
# 记录慢速测试
if duration > 1.0: # 超过1秒算慢速
print(f"⚠️ 慢速测试: {item.nodeid} ({duration:.2f}s)")
示例2:测试环境管理
# conftest.py
import pytest
import requests
from datetime import datetime
def pytest_configure(config):
"""配置阶段:环境检查"""
# 检查测试环境是否可用
try:
response = requests.get("http://test-api:8000/health", timeout=5)
if response.status_code != 200:
pytest.exit("测试环境不可用")
except requests.ConnectionError:
pytest.exit("无法连接到测试环境")
def pytest_sessionstart(session):
"""会话开始:资源准备"""
print(f"测试开始时间: {datetime.now()}")
# 初始化测试数据库
init_test_database()
# 创建测试用户
create_test_users()
def pytest_sessionfinish(session, exitstatus):
"""会话结束:资源清理"""
print(f"测试结束时间: {datetime.now()}")
# 清理测试数据
cleanup_test_data()
# 生成测试报告
generate_test_report(session)
def pytest_unconfigure(config):
"""pytest退出:最终清理"""
# 关闭数据库连接
close_database_connections()
示例3:自定义测试报告生成
# conftest.py
import pytest
import json
from pathlib import Path
class CustomTestReporter:
def __init__(self):
self.results = []
def pytest_runtest_logreport(self, report):
"""收集测试结果"""
if report.when == "call": # 只关注测试执行阶段
test_result = {
"name": report.nodeid,
"outcome": report.outcome,
"duration": getattr(report, 'duration', 0),
"when": report.when,
"screenshot": getattr(report, 'screenshot', None)
}
self.results.append(test_result)
def pytest_sessionfinish(self, session, exitstatus):
"""生成自定义报告"""
report_data = {
"session_id": session.startdir,
"exit_status": exitstatus,
"total_tests": len(self.results),
"passed": len([r for r in self.results if r["outcome"] == "passed"]),
"failed": len([r for r in self.results if r["outcome"] == "failed"]),
"results": self.results
}
# 保存为JSON文件
report_file = Path("custom_test_report.json")
report_file.write_text(json.dumps(report_data, indent=2, ensure_ascii=False))
# 注册自定义报告器
@pytest.hookimpl(trylast=True)
def pytest_configure(config):
config.custom_reporter = CustomTestReporter()
def pytest_runtest_makereport(item, call):
"""为报告添加额外信息"""
if call.when == "call" and call.excinfo:
# 失败时截图
if hasattr(item.obj, 'screenshot'):
call.screenshot = item.obj.screenshot()
示例4:动态跳过测试
# conftest.py
import pytest
import os
def pytest_collection_modifyitems(config, items):
"""动态跳过测试"""
# 跳过条件1:特定平台
if sys.platform != "linux":
skip_linux = pytest.mark.skip(reason="仅Linux平台支持")
for item in items:
if item.get_closest_marker("linux_only"):
item.add_marker(skip_linux)
# 跳过条件2:环境变量控制
if not os.getenv("RUN_SLOW_TESTS"):
skip_slow = pytest.mark.skip(reason="未设置RUN_SLOW_TESTS环境变量")
for item in items:
if item.get_closest_marker("slow"):
item.add_marker(skip_slow)
# 跳过条件3:功能开关
if not is_feature_enabled("new_ui"):
skip_new_ui = pytest.mark.skip(reason="新UI功能未启用")
for item in items:
if "new_ui" in item.nodeid:
item.add_marker(skip_new_ui)
def is_feature_enabled(feature_name):
"""检查功能是否启用"""
# 从配置文件、数据库或环境变量读取
return os.getenv(f"FEATURE_{feature_name.upper()}", "false").lower() == "true"
9. 钩子函数执行顺序
# conftest.py
def pytest_configure(config):
print("1. pytest_configure")
def pytest_sessionstart(session):
print("2. pytest_sessionstart")
def pytest_collection_modifyitems(session, config, items):
print("3. pytest_collection_modifyitems")
def pytest_runtest_protocol(item, nextitem):
print("4. pytest_runtest_protocol")
def pytest_runtest_setup(item):
print("5. pytest_runtest_setup")
def pytest_runtest_call(item):
print("6. pytest_runtest_call")
def pytest_runtest_teardown(item, nextitem):
print("7. pytest_runtest_teardown")
def pytest_sessionfinish(session, exitstatus):
print("8. pytest_sessionfinish")
def pytest_unconfigure(config):
print("9. pytest_unconfigure")
10. 最佳实践
- 合理放置钩子:将钩子函数放在合适的conftest.py中
- 避免副作用:钩子函数应该尽量减少全局副作用
- 错误处理:在钩子中添加适当的错误处理
- 性能考虑:避免在钩子中执行耗时操作
- 文档完善:为自定义钩子添加清晰的文档
钩子函数是 pytest 最强大的功能之一,通过它们可以实现高度定制化的测试流程和行为。
五、阶段4:项目实战(1-2周)
1. API 测试项目结构
api_test_project/
├── conftest.py # 共享 fixture
├── pytest.ini # 配置文件
├── requirements.txt # 依赖包
├── test_data/ # 测试数据
│ ├── users.json
│ └── test_cases.csv
├── tests/ # 测试用例
│ ├── __init__.py
│ ├── conftest.py # 测试目录配置
│ ├── test_user_api.py # 用户 API 测试
│ ├── test_product_api.py # 商品 API 测试
│ └── test_order_api.py # 订单 API 测试
└── utils/ # 工具函数
├── __init__.py
├── api_client.py # API 客户端
└── data_loader.py # 数据加载
2. 完整的 API 测试示例
# tests/test_user_api.py
import pytest
import requests
class TestUserAPI:
"""用户 API 测试套件"""
@pytest.mark.smoke
@pytest.mark.parametrize("user_data", [
{"name": "张三", "email": "zhang@test.com"},
{"name": "李四", "email": "li@test.com"}
])
def test_create_user(self, api_client, user_data):
"""测试创建用户"""
response = api_client.post("/users", json=user_data)
assert response.status_code == 201
assert "id" in response.json()
assert response.json()["name"] == user_data["name"]
def test_get_user(self, api_client, create_test_user):
"""测试获取用户信息"""
user_id = create_test_user["id"]
response = api_client.get(f"/users/{user_id}")
assert response.status_code == 200
assert response.json()["id"] == user_id
def test_update_user(self, api_client, create_test_user):
"""测试更新用户"""
user_id = create_test_user["id"]
update_data = {"name": "更新后的姓名"}
response = api_client.put(f"/users/{user_id}", json=update_data)
assert response.status_code == 200
assert response.json()["name"] == "更新后的姓名"
def test_delete_user(self, api_client, create_test_user):
"""测试删除用户"""
user_id = create_test_user["id"]
response = api_client.delete(f"/users/{user_id}")
assert response.status_code == 204
# 验证用户已删除
get_response = api_client.get(f"/users/{user_id}")
assert get_response.status_code == 404
3. conftest.py 完整配置
# tests/conftest.py
import pytest
import requests
import json
@pytest.fixture(scope="session")
def api_base_url():
"""API 基础地址配置"""
return "https://jsonplaceholder.typicode.com" # 示例 API
@pytest.fixture
def api_client(api_base_url):
"""API 客户端 fixture"""
session = requests.Session()
session.base_url = api_base_url
# 设置公共请求头
session.headers.update({
"Content-Type": "application/json",
"User-Agent": "pytest-api-tests/1.0"
})
yield session
session.close()
@pytest.fixture
def create_test_user(api_client):
"""创建测试用户并返回用户数据"""
user_data = {
"name": "测试用户",
"email": "test@example.com",
"username": "testuser"
}
response = api_client.post("/users", json=user_data)
assert response.status_code == 201
user = response.json()
yield user # 提供给测试使用
# 测试完成后清理
cleanup_response = api_client.delete(f"/users/{user['id']}")
# 忽略清理失败,因为示例 API 可能不支持删除
def load_test_data(filename):
"""加载测试数据"""
with open(f"test_data/{filename}", "r", encoding="utf-8") as f:
if filename.endswith(".json"):
return json.load(f)
# 可以添加其他格式支持
@pytest.fixture
def user_test_data():
"""用户测试数据"""
return load_test_data("users.json")
六、阶段5:最佳实践和高级技巧
1. 配置文件 pytest.ini
# pytest.ini
[pytest]
# 测试文件匹配模式
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 自定义标记
markers =
smoke: 冒烟测试
api: API 测试
slow: 慢速测试
integration: 集成测试
# 默认选项
addopts = -v --strict-markers --tb=short
# 测试目录
testpaths = tests
# 忽略目录
norecursedirs = .git build dist *.egg-info
# 最小版本要求
minversion = 6.0
2. 自定义命令行选项
# conftest.py
def pytest_addoption(parser):
"""添加自定义命令行选项"""
parser.addoption(
"--env",
action="store",
default="test",
help="测试环境: dev, test, prod"
)
parser.addoption(
"--browser",
action="store",
default="chrome",
help="浏览器: chrome, firefox, safari"
)
@pytest.fixture(scope="session")
def env_config(request):
"""根据命令行参数返回环境配置"""
env = request.config.getoption("--env")
configs = {
"dev": {"base_url": "https://dev-api.example.com"},
"test": {"base_url": "https://test-api.example.com"},
"prod": {"base_url": "https://api.example.com"}
}
return configs.get(env, configs["test"])
使用自定义选项:
# 指定测试环境
pytest --env=dev
# 指定浏览器
pytest --browser=firefox
# 组合使用
pytest --env=test --browser=chrome -m smoke
3. CI/CD 集成
GitHub Actions 示例:
# .github/workflows/test.yml
name: Python Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-html pytest-xdist
- name: Run tests with pytest
run: |
pytest tests/ -v --html=report.html --self-contained-html -n auto
- name: Upload test results
uses: actions/upload-artifact@v2
with:
name: test-report-${{ matrix.python-version }}
path: report.html
七、学习资源推荐
1. 官方文档
- pytest 官方文档- 最权威的参考资料
- pytest 示例库- 官方示例
2. 实践项目
# 1. 创建练习项目
mkdir pytest-learning && cd pytest-learning
# 2. 初始化虚拟环境
python -m venv venv
source venv/bin/activate # Linux/Mac
# venv\Scripts\activate # Windows
# 3. 安装依赖
pip install pytest requests selenium
# 4. 创建测试结构并开始练习
3. 学习顺序建议
- 第1周:基础语法 + 简单项目练习
- 第2周:Fixture + 参数化实战
- 第3周:插件系统 + 自定义配置
- 第4周:完整项目实战 + CI/CD 集成
总结
pytest 学习核心要点:
- ✅ 掌握 Fixture - pytest 的灵魂功能
- ✅ 熟练参数化 - 数据驱动测试的关键
- ✅ 理解作用域 - 优化测试执行效率
- ✅ 善用标记 - 灵活控制测试执行
- ✅ 项目实战 - 真实场景应用练习
按照这个路径,你可以在 1个月 内系统掌握 pytest,并能够在实际项目中熟练使用!

浙公网安备 33010602011771号