pytest 使用
test_basic.py
Pytest 完全实战手册
一、核心概念与基础
1.1 Pytest 特性
- 零继承架构:测试类无需继承任何基类
- 命名约定 :
- 测试文件:
test_*.py
或*_test.py
- 测试类:
Test*
开头(推荐但非强制) - 测试方法:
test_*
开头
- 测试文件:
- 智能测试发现:自动收集符合命名规则的测试
- 原生断言 :使用 Python 内置
assert
语句
1.2 基本测试结构
python
# test_basic.py
# 函数式测试
def test_addition():
assert 1 + 1 == 2, "加法计算失败"
# 类式测试
class TestMathOperations:
def test_subtraction(self):
assert 5 - 3 == 2
def test_multiplication(self):
assert 2 * 3 == 6
二、测试执行控制
2.1 命令行核心参数
参数 | 作用 | 示例 |
---|---|---|
-v |
详细输出 | pytest -v |
-s |
显示打印输出 | pytest -s |
-k |
关键字过滤 | pytest -k "add" |
-x |
遇错即停 | pytest -x |
--maxfail=n |
最大失败数 | pytest --maxfail=3 |
-n |
并发执行 | pytest -n 4 |
-q |
静默模式 | pytest -q |
--collect-only |
只收集不执行 | pytest --collect-only |
2.2 精确执行控制
bash
# 执行特定文件
pytest tests/calculator/test_basic.py
# 执行特定类
pytest test_api.py::TestLogin
# 执行特定方法
pytest test_db.py::TestUser::test_create_user
# 组合定位
pytest tests/integration/test_payment.py::TestCreditCard::test_3ds_verification
2.3 通过代码执行
python
# run_tests.py
import pytest
if __name__ == "__main__":
pytest.main([
"-v",
"--maxfail=2",
"tests/security/",
"tests/api/test_login.py::TestOAuth"
])
三、高级测试组织
3.1 夹具系统 (Fixtures)
python
# conftest.py
import pytest
@pytest.fixture(scope="module")
def database():
print("\n=== 建立数据库连接 ===")
db = Database()
yield db
print("\n=== 关闭数据库连接 ===")
db.close()
@pytest.fixture
def admin_user(database):
return database.create_user(role="admin")
3.2 使用夹具
python
class TestAdminPanel:
def test_user_management(self, admin_user, database):
assert admin_user.has_permission("manage_users")
users = database.list_users()
assert len(users) > 0
3.3 参数化测试
python
import pytest
@pytest.mark.parametrize("username,password,expected", [
("admin", "secret", True),
("guest", "123456", False),
("", "", False),
("admin", "wrong", False)
])
def test_login(username, password, expected):
result = login(username, password)
assert result == expected
四、并发与扩展
4.1 并发执行 (pytest-xdist)
bash
# 安装
pip install pytest-xdist
# 使用
pytest -n auto # 自动检测CPU核心数
pytest -n 4 # 指定4个进程
4.2 失败重试 (pytest-rerunfailures)
bash
# 安装
pip install pytest-rerunfailures
# 使用
pytest --reruns 3 # 失败重试3次
pytest --reruns-delay 2 # 每次重试间隔2秒
4.3 测试标记 (Marks)
python
# 定义标记
@pytest.mark.slow
def test_large_data_processing():
...
@pytest.mark.security
class TestFirewall:
...
# 执行标记测试
pytest -m "slow and security"
五、最佳实践
5.1 项目结构
project/
├── src/ # 源代码
├── tests/ # 测试代码
│ ├── unit/ # 单元测试
│ │ ├── test_math.py
│ │ └── test_utils.py
│ ├── integration/ # 集成测试
│ │ ├── test_api.py
│ │ └── test_db.py
│ ├── conftest.py # 全局夹具
│ └── pytest.ini # 配置文件
├── .gitignore
└── requirements.txt
5.2 配置管理 (pytest.ini)
ini
[pytest]
addopts = -v --color=yes
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
security: security related tests
ui: user interface tests
filterwarnings =
ignore:.*deprecated.*:DeprecationWarning
norecursedirs = .venv node_modules build dist
5.3 高级断言技巧
python
# 异常断言
def test_division_by_zero():
with pytest.raises(ZeroDivisionError) as excinfo:
1 / 0
assert "division by zero" in str(excinfo.value)
# 集合比较
def test_api_response():
response = get_api_data()
assert response.status_code == 200
assert response.json() == {
"id": 123,
"name": "Test User",
"roles": ["admin", "editor"]
}
# 模糊匹配
def test_log_output(caplog):
process_data()
assert "Processing completed" in caplog.text
六、常见场景解决方案
6.1 跳过测试
python
@pytest.mark.skip(reason="等待第三方服务更新")
def test_external_api():
...
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8+")
def test_new_feature():
...
6.2 临时目录处理
python
def test_file_processing(tmp_path):
test_file = tmp_path / "test.txt"
test_file.write_text("Hello pytest")
with open(test_file, "r") as f:
content = f.read()
assert content == "Hello pytest"
6.3 测试覆盖率
bash
# 安装
pip install pytest-cov
# 使用
pytest --cov=src --cov-report=html
七、调试技巧
7.1 失败时进入调试
bash
pytest --pdb # 每次失败时进入PDB
pytest --trace # 测试开始时进入PDB
7.2 输出捕获控制
python
def test_debug_output(capsys):
print("调试信息")
captured = capsys.readouterr()
assert "调试信息" in captured.out
7.3 日志捕获
python
def test_logging(caplog):
caplog.set_level(logging.INFO)
logger.info("操作开始")
# 执行操作
assert "操作成功" in [rec.message for rec in caplog.records]
八、插件推荐
- pytest-xdist:分布式测试
- pytest-cov:代码覆盖率
- pytest-rerunfailures:失败重试
- pytest-mock:Mock对象
- pytest-django:Django集成
- pytest-asyncio:异步测试
- pytest-html:HTML报告
- pytest-selenium:浏览器自动化
bash
# 推荐安装组合
pip install pytest pytest-xdist pytest-cov pytest-rerunfailures pytest-mock
二、为什么使用夹具
Pytest 夹具系统详解
什么是夹具系统?
夹具(Fixture)系统是 pytest 最强大的功能之一,它提供了一种可复用的机制来:
- 准备测试环境(如数据库连接、配置文件)
- 管理测试资源(如临时文件、网络连接)
- 执行清理操作(如关闭连接、删除临时数据)
- 共享测试数据(如预定义的用户对象)
为什么需要夹具?
考虑以下场景:
python
# 没有夹具的情况
def test_user_creation():
db = connect_db() # 每个测试都要创建连接
user = db.create_user(name="Alice")
assert user.id is not None
db.close() # 每个测试都要关闭连接
def test_user_deletion():
db = connect_db() # 重复代码
user = db.create_user(name="Bob")
db.delete_user(user.id)
assert not db.user_exists(user.id)
db.close() # 重复代码
问题:
- 大量重复代码
- 资源管理容易出错
- 维护困难
夹具如何解决这些问题?
基本夹具示例
python
import pytest
# 定义夹具
@pytest.fixture
def database():
print("\n=== 建立数据库连接 ===")
db = Database()
yield db # 将对象提供给测试用例
print("\n=== 关闭数据库连接 ===")
db.close()
# 使用夹具
def test_user_creation(database): # 通过参数注入夹具
user = database.create_user(name="Alice")
assert user.id is not None
执行流程
1. 测试开始前: 执行 yield 之前的代码 (建立连接)
2. 执行测试用例: 使用数据库对象
3. 测试结束后: 执行 yield 之后的代码 (关闭连接)
夹具的核心特性
1. 作用域控制
通过 scope
参数管理资源生命周期:
作用域 | 描述 | 使用场景 |
---|---|---|
function |
每个测试函数执行一次 | 默认值,适合轻量资源 |
class |
每个测试类执行一次 | 类中多个测试共享资源 |
module |
每个模块执行一次 | 模块级共享资源 |
package |
每个包执行一次 | 跨模块共享资源 |
session |
整个测试会话一次 | 全局资源(如登录会话) |
python
@pytest.fixture(scope="module")
def shared_resource():
print("\n初始化模块级资源")
resource = Resource()
yield resource
print("\n清理模块级资源")
2. 夹具依赖
夹具可以依赖其他夹具:
python
@pytest.fixture
def admin_user(database): # 依赖 database 夹具
return database.create_user(role="admin")
def test_admin_permissions(admin_user):
assert admin_user.has_permission("admin_panel")
3. 自动使用夹具
无需显式声明即可自动应用:
python
@pytest.fixture(autouse=True)
def setup_logging():
logging.basicConfig(level=logging.INFO)
print("\n日志系统已初始化")
# 所有测试都会自动应用此夹具
def test_example():
assert True
4. 参数化夹具
动态生成不同测试场景:
python
@pytest.fixture(params=["admin", "editor", "viewer"])
def user_role(request):
return request.param # 访问参数值
def test_role_permissions(user_role):
assert get_permissions(user_role) != []
高级夹具用法
1. 工厂模式夹具
创建多个实例:
python
@pytest.fixture
def user_factory(database):
def create_user(name, role="user"):
return database.create_user(name=name, role=role)
return create_user # 返回工厂函数
def test_user_roles(user_factory):
admin = user_factory("Admin", role="admin")
guest = user_factory("Guest")
assert admin.is_admin()
assert not guest.is_admin()
2. 动态资源管理
python
@pytest.fixture
def temp_config(tmp_path):
# 使用内置的 tmp_path 夹具
config_file = tmp_path / "config.ini"
config_file.write_text("[DEFAULT]\nlang=en_US")
return config_file
def test_config_loading(temp_config):
config = load_config(temp_config)
assert config["DEFAULT"]["lang"] == "en_US"
3. 夹具重写
在特定位置覆盖夹具:
python
# conftest.py (全局)
@pytest.fixture
def database():
return RealDatabase()
# test_dev.py (局部覆盖)
@pytest.fixture
def database():
return MockDatabase() # 使用模拟数据库
def test_with_mock_db(database):
assert isinstance(database, MockDatabase)
最佳实践
1. 合理组织夹具
使用 conftest.py
文件管理共享夹具:
project/
├── conftest.py # 全局夹具
├── database/
│ ├── conftest.py # 数据库相关夹具
│ └── test_queries.py
└── api/
├── conftest.py # API 测试夹具
└── test_endpoints.py
2. 命名规范
- 夹具函数:
名词
(如database
,admin_user
) - 测试函数:
test_动词
(如test_create_user
)
3. 避免过度使用
- 只在需要共享资源时使用夹具
- 简单测试可直接在测试函数内准备数据
常用内置夹具
夹具名称 | 用途 |
---|---|
tmp_path |
创建临时目录(返回 Path 对象) |
tmpdir |
创建临时目录(返回 py.path) |
capsys |
捕获 stdout/stderr |
caplog |
捕获日志输出 |
monkeypatch |
临时修改环境/对象 |
request |
访问测试上下文信息 |
python
def test_output_capture(capsys):
print("Hello, pytest!")
captured = capsys.readouterr()
assert "pytest" in captured.out
总结:夹具系统的价值
- 资源管理:自动化管理测试资源的生命周期
- 代码复用:消除重复的初始化和清理代码
- 依赖注入:解耦测试逻辑与测试环境
- 可维护性:集中管理环境配置,一处修改全局生效
- 灵活性:支持多种作用域和组合方式
通过夹具系统,pytest 实现了测试环境的声明式管理,让开发者能专注于测试逻辑本身,而不是繁琐的环境准备工作。
三、参数话是怎么实现的
Pytest 参数化测试详解
参数化测试是 pytest 最强大的功能之一,它允许您使用不同的输入数据运行同一个测试逻辑,从而显著减少重复代码并提高测试覆盖率。下面通过多个实际示例详细说明参数化实现方式:
1. 基本参数化实现
使用 @pytest.mark.parametrize
装饰器
python
import pytest
# 简单的加法测试参数化
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), # 测试用例1
(0, 0, 0), # 测试用例2
(-1, 1, 0), # 测试用例3
(10, -5, 5) # 测试用例4
])
def test_addition(a, b, expected):
"""测试加法函数"""
assert a + b == expected
执行结果:
test_math.py::test_addition[1-2-3] PASSED
test_math.py::test_addition[0-0-0] PASSED
test_math.py::test_addition[-1-1-0] PASSED
test_math.py::test_addition[10--5-5] PASSED
2. 参数化实现原理
pytest 的参数化本质上是一个测试生成器:
- 解析
parametrize
装饰器的参数 - 为每组参数创建独立的测试用例
- 每个用例拥有唯一的ID(可自定义)
- 执行时按顺序运行所有生成的测试
3. 高级参数化技巧
3.1 参数化类方法
python
class TestMathOperations:
@pytest.mark.parametrize("x, y, expected", [
(4, 2, 2),
(9, 3, 3),
(15, 5, 3)
])
def test_division(self, x, y, expected):
assert x / y == expected
3.2 多参数组合(笛卡尔积)
python
@pytest.mark.parametrize("a", [1, 10, 100])
@pytest.mark.parametrize("b", [2, 20, 200])
def test_multiplication(a, b):
"""测试所有组合:1×2, 1×20, 1×200, 10×2..."""
assert a * b == a * b # 实际项目中应有具体逻辑
3.3 参数化夹具(动态生成测试数据)
python
import pytest
# 动态生成用户数据的夹具
@pytest.fixture(params=[
("admin", "secret123"),
("editor", "edit_pass"),
("viewer", "view_only")
])
def user_credentials(request):
return request.param
def test_login(user_credentials):
username, password = user_credentials
# 执行登录逻辑
assert login(username, password) is True
4. 参数化高级应用
4.1 自定义测试ID
python
@pytest.mark.parametrize(
"input, expected",
[
("3+5", 8),
("2*4", 8),
("6/2", 3),
("10-3", 7)
],
ids=[
"加法测试",
"乘法测试",
"除法测试",
"减法测试"
]
)
def test_eval(input, expected):
assert eval(input) == expected
4.2 从外部文件加载参数
python
import json
import pytest
def load_test_data():
with open("test_data.json") as f:
return json.load(f)
# 从JSON文件加载测试数据
@pytest.mark.parametrize("data", load_test_data())
def test_with_external_data(data):
result = process_data(data["input"])
assert result == data["expected"]
4.3 条件参数化
python
import sys
import pytest
# 根据环境条件选择参数
params = []
if sys.platform == "win32":
params.append(("Windows", "C:\\"))
else:
params.append(("Linux", "/home"))
@pytest.mark.parametrize("os_name, home_dir", params)
def test_platform_specific(os_name, home_dir):
assert get_home_directory() == home_dir
5. 参数化最佳实践
5.1 保持参数化数据简洁
python
# 推荐:使用变量提高可读性
VALID_EMAILS = [
"[email protected]",
"[email protected]",
"[email protected]"
]
INVALID_EMAILS = [
"invalid",
"missing@domain",
"@domain.com"
]
@pytest.mark.parametrize("email", VALID_EMAILS)
def test_valid_email(email):
assert validate_email(email)
@pytest.mark.parametrize("email", INVALID_EMAILS)
def test_invalid_email(email):
assert not validate_email(email)
5.2 组合参数化与夹具
python
import pytest
@pytest.fixture
def calculator():
return Calculator()
# 参数化与夹具结合
@pytest.mark.parametrize("a, b, expected", [
(5, 3, 8),
(10, -2, 8),
(0, 0, 0)
])
def test_calculator_add(calculator, a, b, expected):
assert calculator.add(a, b) == expected
5.3 处理异常的参数化
python
import pytest
@pytest.mark.parametrize("a, b, exception", [
(1, 0, ZeroDivisionError),
("text", 2, TypeError),
(None, 5, ValueError)
])
def test_division_errors(a, b, exception):
with pytest.raises(exception):
divide(a, b)
6. 参数化在实战中的应用
场景:测试用户权限系统
python
import pytest
# 用户角色数据
ROLES = ["admin", "editor", "viewer", "guest"]
# 权限矩阵
PERMISSIONS = {
"create": [True, True, False, False],
"delete": [True, False, False, False],
"edit": [True, True, True, False],
"view": [True, True, True, True]
}
# 生成参数化数据
def generate_permission_tests():
for role_idx, role in enumerate(ROLES):
for permission, access_list in PERMISSIONS.items():
expected = access_list[role_idx]
yield role, permission, expected
@pytest.mark.parametrize("role, permission, expected", list(generate_permission_tests()))
def test_permission_access(role, permission, expected):
user = create_user(role=role)
assert user.has_permission(permission) == expected
执行结果示例:
test_permissions.py::test_permission_access[admin-create-True] PASSED
test_permissions.py::test_permission_access[admin-delete-True] PASSED
test_permissions.py::test_permission_access[admin-edit-True] PASSED
test_permissions.py::test_permission_access[admin-view-True] PASSED
test_permissions.py::test_permission_access[editor-create-True] PASSED
test_permissions.py::test_permission_access[editor-delete-False] PASSED
...
总结:参数化测试的核心价值
- 减少重复代码:相同测试逻辑只需编写一次
- 提高覆盖率:轻松测试多种边界条件和输入组合
- 测试数据分离:将测试数据与测试逻辑解耦
- 清晰报告:每个参数组合作为独立测试显示
- 灵活扩展:轻松添加新测试用例
参数化测试是 pytest 区别于其他测试框架的核心优势之一,通过将测试数据从测试逻辑中分离出来,您可以创建更简洁、更强大且更易维护的测试套件。
四、怎么实现和代码分析的参数话
Pytest 参数化测试数据分离实践
将参数化测试数据与测试代码分离是提高测试可维护性的重要实践。下面详细介绍多种实现方式,并提供完整示例:
1. JSON 文件存储测试数据
数据文件:test_data/addition.json
json
[
{"a": 1, "b": 2, "expected": 3},
{"a": 0, "b": 0, "expected": 0},
{"a": -1, "b": 1, "expected": 0},
{"a": 10, "b": -5, "expected": 5},
{"a": 100, "b": 200, "expected": 300}
]
测试代码:test_math.py
python
import json
import pytest
def load_json_data(file_name):
"""从JSON文件加载测试数据"""
with open(f"test_data/{file_name}", encoding="utf-8") as f:
return json.load(f)
# 使用JSON参数化
@pytest.mark.parametrize(
"data",
load_json_data("addition.json"),
ids=lambda d: f"{d['a']}+{d['b']}={d['expected']}"
)
def test_addition_json(data):
assert data["a"] + data["b"] == data["expected"]
2. YAML 文件存储测试数据(需要安装PyYAML)
安装依赖:
bash
pip install pyyaml
数据文件:test_data/login.yaml
yaml
- username: "admin"
password: "secure_pass"
expected: true
scenario: "有效管理员账户"
- username: "editor"
password: "edit123"
expected: true
scenario: "有效编辑账户"
- username: "invalid_user"
password: "wrong_pass"
expected: false
scenario: "无效凭据"
- username: ""
password: "empty_user"
expected: false
scenario: "空用户名"
- username: "admin"
password: ""
expected: false
scenario: "空密码"
测试代码:test_auth.py
python
import yaml
import pytest
def load_yaml_data(file_name):
"""从YAML文件加载测试数据"""
with open(f"test_data/{file_name}", encoding="utf-8") as f:
return yaml.safe_load(f)
# 使用YAML参数化
@pytest.mark.parametrize(
"test_data",
load_yaml_data("login.yaml"),
ids=lambda d: d["scenario"]
)
def test_login_yaml(test_data):
result = login_user(test_data["username"], test_data["password"])
assert result == test_data["expected"]
3. CSV 文件存储测试数据
数据文件:test_data/multiplication.csv
csv
a,b,expected,description
2,3,6,正整数
0,5,0,零乘数
-4,3,-12,负数乘正数
-2,-3,6,负负得正
10,0.5,5,小数乘法
测试代码:test_math.py
python
import csv
import pytest
def load_csv_data(file_name):
"""从CSV文件加载测试数据"""
data = []
with open(f"test_data/{file_name}", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
# 转换数值类型
row["a"] = float(row["a"]) if "." in row["a"] else int(row["a"])
row["b"] = float(row["b"]) if "." in row["b"] else int(row["b"])
row["expected"] = float(row["expected"]) if "." in row["expected"] else int(row["expected"])
data.append(row)
return data
# 使用CSV参数化
@pytest.mark.parametrize(
"data",
load_csv_data("multiplication.csv"),
ids=lambda d: d["description"]
)
def test_multiplication_csv(data):
assert data["a"] * data["b"] == data["expected"]
4. Python 模块存储测试数据
数据文件:test_data/user_data.py
python
# 用户创建测试数据
USER_CREATION_DATA = [
{
"name": "Alice Johnson",
"email": "[email protected]",
"role": "admin",
"expected_status": 201
},
{
"name": "Bob Smith",
"email": "[email protected]",
"role": "editor",
"expected_status": 201
},
{
"name": "Invalid User",
"email": "not-an-email",
"role": "viewer",
"expected_status": 400,
"expected_error": "Invalid email format"
},
{
"name": "",
"email": "[email protected]",
"role": "guest",
"expected_status": 400,
"expected_error": "Name cannot be empty"
}
]
# 用户删除测试数据
USER_DELETION_DATA = [
(1, 204), # 有效用户ID
(999, 404), # 不存在的用户ID
("invalid", 400) # 无效ID格式
]
测试代码:test_user_api.py
python
import pytest
from test_data import user_data
# 用户创建测试
@pytest.mark.parametrize(
"user_data",
user_data.USER_CREATION_DATA,
ids=lambda d: d["name"] or "Empty Name"
)
def test_create_user(user_data):
response = create_user_api(
name=user_data["name"],
email=user_data["email"],
role=user_data["role"]
)
assert response.status_code == user_data["expected_status"]
if user_data["expected_status"] != 201:
error_data = response.json()
assert user_data["expected_error"] in error_data["message"]
# 用户删除测试
@pytest.mark.parametrize(
"user_id, expected_status",
user_data.USER_DELETION_DATA,
ids=lambda x: f"ID={x[0]}-Status={x[1]}"
)
def test_delete_user(user_id, expected_status):
response = delete_user_api(user_id)
assert response.status_code == expected_status
5. 数据库存储测试数据
测试代码:test_db_integration.py
python
import pytest
import sqlite3
@pytest.fixture(scope="module")
def test_db():
"""创建内存中的测试数据库"""
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
# 创建测试表
cursor.execute("""
CREATE TABLE test_data (
id INTEGER PRIMARY KEY,
input_a INTEGER,
input_b INTEGER,
expected INTEGER,
test_name TEXT
)
""")
# 插入测试数据
test_cases = [
(1, 2, 3, "Positive numbers"),
(0, 0, 0, "Zeros"),
(-1, 1, 0, "Negative and positive"),
(10, -5, 5, "Positive and negative")
]
cursor.executemany(
"INSERT INTO test_data (input_a, input_b, expected, test_name) VALUES (?, ?, ?, ?)",
test_cases
)
conn.commit()
yield conn
conn.close()
def load_db_data(db_conn):
"""从数据库加载测试数据"""
cursor = db_conn.cursor()
cursor.execute("SELECT input_a, input_b, expected, test_name FROM test_data")
return cursor.fetchall()
# 使用数据库参数化
@pytest.mark.parametrize(
"a, b, expected, test_name",
load_db_data(test_db()),
ids=lambda x: x[3] # 使用test_name作为ID
)
def test_addition_db(a, b, expected, test_name):
assert a + b == expected
6. 动态生成测试数据
测试代码:test_data_generators.py
python
import pytest
import random
def generate_performance_test_data():
"""生成性能测试数据"""
for size in [10, 100, 1000, 10000]:
# 生成测试数据集
data = [random.randint(1, 1000) for _ in range(size)]
# 预期结果:排序后的列表
expected = sorted(data)
yield pytest.param(
data, expected,
id=f"Size_{size}"
)
# 动态参数化性能测试
@pytest.mark.parametrize(
"input_data, expected",
generate_performance_test_data()
)
def test_sorting_performance(input_data, expected):
result = sorted(input_data) # 实际排序操作
assert result == expected
7. 混合数据源参数化
测试代码:test_mixed_sources.py
python
import pytest
from test_data import user_data
from .data_loader import load_json_data, load_csv_data
# 从多个来源组合测试数据
def combined_test_data():
# 来源1:JSON文件
for data in load_json_data("api_test_cases.json"):
yield pytest.param(data, id=f"JSON-{data['case_id']}")
# 来源2:Python模块
for data in user_data.API_TEST_CASES:
yield pytest.param(data, id=f"Module-{data['id']}")
# 来源3:CSV文件
for data in load_csv_data("additional_cases.csv"):
yield pytest.param(data, id=f"CSV-{data['test_id']}")
# 使用混合数据源
@pytest.mark.parametrize(
"test_case",
combined_test_data()
)
def test_api_endpoint(test_case):
response = call_api(
endpoint=test_case["endpoint"],
method=test_case["method"],
data=test_case["payload"]
)
assert response.status_code == test_case["expected_status"]
if "expected_data" in test_case:
assert response.json() == test_case["expected_data"]
最佳实践总结
-
选择合适的数据格式:
- 简单结构:JSON/YAML
- 表格数据:CSV
- 复杂逻辑:Python模块
- 动态数据:数据库/生成器
-
数据文件组织:
project/ ├── src/ ├── tests/ │ ├── test_modules/ │ ├── test_data/ # 所有数据文件 │ │ ├── json/ │ │ ├── yaml/ │ │ ├── csv/ │ │ └── __init__.py # Python数据模块 │ └── conftest.py
-
数据加载器封装:
python# tests/data_loader.py import json import csv import yaml import os DATA_DIR = os.path.join(os.path.dirname(__file__), "test_data") def load_json(relative_path): path = os.path.join(DATA_DIR, relative_path) with open(path, encoding="utf-8") as f: return json.load(f) def load_yaml(relative_path): path = os.path.join(DATA_DIR, relative_path) with open(path, encoding="utf-8") as f: return yaml.safe_load(f) # 其他加载函数...
-
数据验证:
- 使用JSON Schema验证数据结构
- 添加数据版本控制
- 编写数据完整性检查脚本
-
数据与测试分离优势:
- 非技术人员可维护测试数据
- 数据变更不影响测试逻辑
- 易于添加新的测试用例
- 支持数据驱动的大规模测试
- 提高测试代码可读性
通过以上方法,您可以实现测试数据与测试代码的清晰分离,创建更健壮、更易维护的测试套件,同时保持测试的灵活性和可扩展性。
四、 怎么写测试报错
Pytest 测试报告生成完全指南
测试报告是将测试结果可视化展示的关键工具,下面详细介绍多种生成专业测试报告的方法和工具:
一、内置报告输出
1. 控制台基础报告
bash
# 详细模式(显示每个测试用例)
pytest -v
# 显示打印输出
pytest -s
# 组合使用
pytest -sv
2. 测试结果摘要
bash
pytest -ra # 显示所有结果摘要
pytest -rA # 显示详细摘要(包括通过用例)
输出示例:
========================= short test summary info =========================
PASSED test_math.py::test_addition[1-2-3]
PASSED test_math.py::test_addition[0-0-0]
FAILED test_math.py::test_addition[-1-1-0] - assert (-1 + 1) == 0
PASSED test_auth.py::test_login[admin-secure_pass-True]
二、HTML 测试报告 (pytest-html)
1. 安装插件
bash
pip install pytest-html
2. 生成基础报告
bash
pytest --html=report.html
3. 高级配置
bash
# 包含额外信息(环境、CSS等)
pytest --html=report.html --self-contained-html
# 添加元数据
pytest --html=report.html \
--metadata Python-version $(python --version) \
--metadata Platform $(uname -a)
4. 自定义报告内容
在 conftest.py
中添加钩子:
python
def pytest_html_report_title(report):
report.title = "自动化测试报告 - 2023"
def pytest_html_results_summary(prefix, summary, postfix):
prefix.extend([html.p("项目: 电商系统V2.0")])
summary.extend([html.h2("关键指标统计")])
def pytest_html_results_table_row(report, cells):
if report.passed:
cells.insert(1, html.td("✅"))
elif report.failed:
cells.insert(1, html.td("❌"))
三、Allure 高级报告
1. 安装与配置
bash
# 安装Allure框架
# macOS: brew install allure
# Windows: scoop install allure
# 安装pytest插件
pip install allure-pytest
2. 生成报告
bash
# 运行测试并收集结果
pytest --alluredir=./allure-results
# 生成HTML报告
allure generate ./allure-results -o ./allure-report --clean
# 打开报告
allure open ./allure-report
3. 增强报告内容
在测试代码中添加丰富信息:
python
import allure
import pytest
@allure.epic("认证模块")
@allure.feature("用户登录")
class TestLogin:
@allure.story("成功登录场景")
@allure.title("管理员登录测试")
@allure.severity(allure.severity_level.CRITICAL)
@pytest.mark.parametrize("username,password", [("admin", "secure_pass")])
def test_admin_login(self, username, password):
with allure.step("输入用户名和密码"):
allure.attach(f"用户名: {username}", "输入信息")
allure.attach(f"密码: {password}", "输入信息", allure.attachment_type.TEXT)
with allure.step("点击登录按钮"):
pass # 模拟点击操作
with allure.step("验证登录成功"):
result = login(username, password)
assert result is True
allure.attach.file('./screenshots/login_success.png',
name='登录成功截图',
attachment_type=allure.attachment_type.PNG)
@allure.story("失败登录场景")
@allure.title("错误密码登录测试")
def test_failed_login(self):
with allure.step("输入错误密码"):
result = login("admin", "wrong_pass")
with allure.step("验证登录失败"):
assert result is False
allure.attach("""
<h4>预期行为:</h4>
<ul>
<li>显示错误提示</li>
<li>登录按钮禁用3秒</li>
</ul>
""", "验证点", allure.attachment_type.HTML)
四、JUnit XML 报告(持续集成集成)
1. 生成XML报告
bash
pytest --junitxml=test-results.xml
2. XML报告示例
xml
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="pytest" errors="0" failures="1" skipped="0" tests="4" time="0.5">
<testcase classname="test_math" name="test_addition[1-2-3]" time="0.1"/>
<testcase classname="test_math" name="test_addition[0-0-0]" time="0.05"/>
<testcase classname="test_math" name="test_addition[-1-1-0]" time="0.1">
<failure message="AssertionError: assert (-1 + 1) == 0">...</failure>
</testcase>
<testcase classname="test_auth" name="test_login" time="0.25"/>
</testsuite>
</testsuites>
3. 与CI工具集成
在Jenkins中使用JUnit插件:
groovy
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'pytest --junitxml=test-results.xml'
}
}
stage('Report') {
steps {
junit 'test-results.xml'
}
}
}
}
五、自定义报告系统
1. 使用pytest钩子收集结果
python
# conftest.py
import json
from datetime import datetime
def pytest_sessionfinish(session, exitstatus):
"""测试结束后生成自定义报告"""
report_data = {
"timestamp": datetime.now().isoformat(),
"total": session.testscollected,
"passed": session.testscollected - session.testsfailed - session.testsskipped,
"failed": session.testsfailed,
"skipped": session.testsskipped,
"duration": round(session.duration, 2),
"test_details": []
}
# 遍历所有测试项
for item in session.items:
report_data["test_details"].append({
"nodeid": item.nodeid,
"outcome": item.report.outcome if hasattr(item, 'report') else "unknown",
"duration": getattr(item.report, 'duration', 0),
"error": getattr(item.report, 'longreprtext', '') if hasattr(item.report, 'longreprtext') else ''
})
# 保存为JSON文件
with open("custom_report.json", "w") as f:
json.dump(report_data, f, indent=2)
2. 生成可视化报告
使用Python创建HTML报告:
python
# generate_report.py
import json
from jinja2 import Template
# 加载测试数据
with open('custom_report.json') as f:
report_data = json.load(f)
# HTML模板
html_template = """
<!DOCTYPE html>
<html>
<head>
<title>测试报告 - {{ timestamp }}</title>
<style>
.passed { color: green; }
.failed { color: red; }
.skipped { color: orange; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
tr:nth-child(even) { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>自动化测试报告</h1>
<p>执行时间: {{ timestamp }}</p>
<div class="summary">
<h2>测试概览</h2>
<p>总用例数: {{ total }}</p>
<p>通过: <span class="passed">{{ passed }}</span></p>
<p>失败: <span class="failed">{{ failed }}</span></p>
<p>跳过: <span class="skipped">{{ skipped }}</span></p>
<p>执行时长: {{ duration }} 秒</p>
</div>
<div class="details">
<h2>测试详情</h2>
<table>
<tr>
<th>测试用例</th>
<th>状态</th>
<th>时长(秒)</th>
<th>错误信息</th>
</tr>
{% for test in test_details %}
<tr>
<td>{{ test.nodeid }}</td>
<td class="{{ test.outcome }}">{{ test.outcome }}</td>
<td>{{ test.duration | round(3) }}</td>
<td>{{ test.error | truncate(200) }}</td>
</tr>
{% endfor %}
</table>
</div>
</body>
</html>
"""
# 渲染报告
template = Template(html_template)
html_report = template.render(**report_data)
# 保存报告
with open("custom_report.html", "w") as f:
f.write(html_report)
print("自定义报告已生成: custom_report.html")
六、商业与开源报告工具对比
工具/插件 | 类型 | 特点 | 适用场景 |
---|---|---|---|
pytest-html | 开源 | 简单易用,无需额外依赖 | 快速生成基础报告 |
Allure | 开源 | 高度可定制,支持步骤展示 | 企业级测试报告 |
ReportPortal | 开源 | 实时分析,AI辅助 | 大型持续集成环境 |
Xray | 商业 | Jira集成,需求跟踪 | Jira用户 |
TestRail | 商业 | 测试管理集成 | 专业测试团队 |
Qase | 商业 | 现代化界面,丰富API | SaaS测试管理 |
七、最佳实践建议
-
分层报告策略:
- 开发环境:控制台报告(快速反馈)
- CI流水线:JUnit XML + Allure(自动化分析)
- 团队分享:HTML/PDF格式(易读易分享)
-
报告内容优化:
python# 添加截图和日志 @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: # 失败时截图 driver = item.funcargs.get("browser") if driver: screenshot = driver.get_screenshot_as_base64() html = f'<div><img src="data:image/png;base64,{screenshot}"></div>' report.extra = [pytest_html.extras.html(html)] # 附加日志 with open("test.log", "r") as log_file: report.extra.append(pytest_html.extras.text(log_file.read(), "日志"))
-
定期清理:
bash# 保留最近7天的报告 find ./reports -name "*.html" -mtime +7 -exec rm {} \;
-
报告安全:
- 敏感信息脱敏处理
- 访问权限控制
- HTTPS传输报告
-
自动化归档:
bash# 示例:测试后自动归档报告 pytest --html=reports/$(date +%Y%m%d).html
八、完整工作流示例
bash
# 1. 运行测试并生成多种报告
pytest \
--html=reports/$(date +%Y%m%d_%H%M).html \
--junitxml=reports/junit-results.xml \
--alluredir=reports/allure-results
# 2. 生成Allure报告
allure generate reports/allure-results -o reports/allure-report --clean
# 3. 归档报告
zip -r reports_$(date +%Y%m%d).zip reports/
# 4. 发送通知
python send_notification.py --report reports/latest.html
通过合理选择报告工具和优化报告内容,您可以创建信息丰富、直观易懂的测试报告,有效提升团队对产品质量的可视性和信心。
六、 综述
Pytest 全功能测试用例实战教程
下面是一套完整的测试用例,涵盖了 pytest 的核心功能和高级特性。每个测试用例都包含详细注释,帮助您全面掌握 pytest 的使用方法。
python
"""
Pytest 全功能实战测试套件
包含 pytest 核心功能、高级特性和最佳实践
"""
import pytest
import logging
import sys
import os
from datetime import datetime
# 设置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 示例业务逻辑函数
def add(a, b):
"""加法函数"""
return a + b
def divide(a, b):
"""除法函数"""
if b == 0:
raise ValueError("除数不能为零")
return a / b
def create_user(username, role="user"):
"""创建用户"""
if not username:
raise ValueError("用户名不能为空")
return {"username": username, "role": role, "created_at": datetime.now()}
# ======================
# 1. 基础测试用例
# ======================
def test_addition_basic():
"""基础测试示例"""
result = add(2, 3)
assert result == 5, "2+3应该等于5"
class TestMathOperations:
"""测试类示例"""
def test_addition_in_class(self):
"""类中的测试方法"""
assert add(10, 20) == 30
def test_negative_addition(self):
"""负数加法测试"""
assert add(-5, -3) == -8
# ======================
# 2. 参数化测试
# ======================
@pytest.mark.parametrize("a, b, expected", [
(1, 2, 3), # 正整数
(0, 0, 0), # 零
(-1, 1, 0), # 负数与正数
(2.5, 3.5, 6.0) # 浮点数
], ids=["正整数", "零", "负数与正数", "浮点数"])
def test_parametrized_addition(a, b, expected):
"""参数化加法测试"""
result = add(a, b)
assert result == expected, f"{a}+{b}应该等于{expected},实际得到{result}"
# ======================
# 3. 夹具系统 (Fixtures)
# ======================
@pytest.fixture(scope="module")
def database_connection():
"""模块级夹具 - 模拟数据库连接"""
logger.info("\n=== 建立数据库连接 ===")
# 这里模拟数据库连接
db = {"connected": True, "users": []}
yield db # 测试中使用这个对象
logger.info("\n=== 关闭数据库连接 ===")
# 清理操作
db["connected"] = False
@pytest.fixture
def admin_user(database_connection):
"""函数级夹具 - 创建管理员用户"""
user = create_user("admin_user", "admin")
database_connection["users"].append(user)
return user
@pytest.fixture
def regular_user(database_connection):
"""函数级夹具 - 创建普通用户"""
user = create_user("regular_user")
database_connection["users"].append(user)
return user
def test_user_creation(admin_user, regular_user):
"""测试用户创建"""
assert admin_user["role"] == "admin"
assert regular_user["role"] == "user"
assert "created_at" in admin_user
def test_database_connection(database_connection):
"""测试数据库连接"""
assert database_connection["connected"] is True
# ======================
# 4. 标记 (Marks) 和跳过
# ======================
@pytest.mark.slow
def test_slow_operation():
"""标记为慢速测试"""
# 模拟耗时操作
import time
time.sleep(2)
assert True
@pytest.mark.skip(reason="功能尚未实现")
def test_unimplemented_feature():
"""跳过未实现的功能测试"""
assert False, "这个测试不应该执行"
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8+")
def test_python38_feature():
"""条件跳过测试"""
# Python 3.8+ 的特性
assert (x := 5) == 5 # 海象运算符
@pytest.mark.xfail(reason="已知问题,待修复")
def test_known_bug():
"""预期失败的测试"""
assert add(0.1, 0.2) == 0.3 # 浮点数精度问题
# ======================
# 5. 异常测试
# ======================
def test_divide_by_zero():
"""测试除零异常"""
with pytest.raises(ValueError) as exc_info:
divide(10, 0)
assert "除数不能为零" in str(exc_info.value)
@pytest.mark.parametrize("username", ["", None, " "])
def test_invalid_username(username):
"""测试无效用户名"""
with pytest.raises(ValueError) as exc_info:
create_user(username)
assert "用户名不能为空" in str(exc_info.value)
# ======================
# 6. 临时文件和目录
# ======================
def test_file_operations(tmp_path):
"""测试临时文件操作"""
# 创建临时文件
test_file = tmp_path / "test.txt"
test_file.write_text("Hello pytest!")
# 读取文件内容
content = test_file.read_text()
assert content == "Hello pytest!"
# 创建子目录
sub_dir = tmp_path / "subdir"
sub_dir.mkdir()
assert sub_dir.exists()
# ======================
# 7. 输出捕获
# ======================
def test_output_capture(capsys):
"""测试输出捕获"""
print("标准输出消息")
sys.stderr.write("标准错误消息")
# 捕获输出
captured = capsys.readouterr()
assert "标准输出消息" in captured.out
assert "标准错误消息" in captured.err
def test_log_capture(caplog):
"""测试日志捕获"""
caplog.set_level(logging.INFO)
logger.info("这是一条信息日志")
logger.warning("这是一条警告日志")
assert "信息日志" in caplog.text
assert "警告日志" in [rec.message for rec in caplog.records]
# ======================
# 8. 并发测试 (需要pytest-xdist)
# ======================
@pytest.mark.parametrize("index", range(10))
def test_concurrent_execution(index):
"""模拟并发测试"""
import time
time.sleep(0.1) # 模拟工作负载
assert index < 10 # 总是成功
# ======================
# 9. Allure 报告增强
# ======================
@pytest.mark.allure
class TestAllureFeatures:
"""Allure 报告增强功能测试"""
@pytest.mark.parametrize("a,b,expected", [(2, 3, 5), (5, 5, 10)])
def test_parametrized_with_allure(self, a, b, expected):
"""参数化测试与Allure结合"""
result = add(a, b)
assert result == expected
# Allure 步骤
if hasattr(pytest, 'allure'):
import allure
with allure.step("验证加法结果"):
allure.attach(f"{a} + {b} = {result}", "计算详情")
def test_allure_attachments(self):
"""测试Allure附件功能"""
if hasattr(pytest, 'allure'):
import allure
# 添加文本附件
allure.attach("这是一段文本附件", name="文本附件", attachment_type=allure.attachment_type.TEXT)
# 添加HTML附件
allure.attach("<h1>HTML附件</h1><p>这是一个HTML格式的附件</p>",
name="HTML附件",
attachment_type=allure.attachment_type.HTML)
# ======================
# 10. 自定义标记和分组
# ======================
@pytest.mark.integration
def test_integration_feature():
"""集成测试标记"""
assert True
@pytest.mark.security
class TestSecurityFeatures:
"""安全测试分组"""
def test_authentication(self):
"""认证测试"""
assert True
def test_authorization(self):
"""授权测试"""
assert True
# ======================
# 11. 夹具参数化
# ======================
@pytest.fixture(params=["admin", "editor", "viewer"])
def user_role(request):
"""参数化夹具 - 不同用户角色"""
return request.param
def test_role_based_access(user_role):
"""测试基于角色的访问控制"""
if user_role == "admin":
assert True # 管理员有完全访问权限
elif user_role == "editor":
assert True # 编辑者有部分权限
else:
assert True # 查看者只有读取权限
# ======================
# 12. 测试配置 (pytest.ini)
# ======================
# 实际项目中会在 pytest.ini 中配置
# 这里仅作演示
def test_config_usage():
"""测试配置使用"""
# 通常用于检查标记或配置选项
assert "integration" in [mark.name for mark in test_integration_feature.pytestmark]
# ======================
# 13. 测试选择和过滤
# ======================
# 这些测试用于演示选择功能,实际通过命令行执行
def test_select_by_keyword():
"""可通过关键字选择的测试"""
assert True
def test_another_selectable_test():
"""另一个可选择测试"""
assert True
# ======================
# 14. 自定义钩子和插件
# ======================
# 在 conftest.py 中实现
# 这里仅作演示
def test_custom_hook():
"""测试自定义钩子(通常在conftest中实现)"""
# 实际项目中可能有自定义行为
assert True
# ======================
# 15. 测试覆盖率 (需要pytest-cov)
# ======================
def test_coverage_important_function():
"""重要功能测试(用于覆盖率)"""
# 测试业务关键函数
assert add(100, 200) == 300
assert divide(10, 2) == 5.0
# 测试边界情况
with pytest.raises(ValueError):
divide(5, 0)
# ======================
# 16. 猴子补丁 (Monkeypatch)
# ======================
def test_monkeypatch_example(monkeypatch):
"""使用猴子补丁修改环境"""
# 修改环境变量
monkeypatch.setenv("APP_ENV", "testing")
assert os.getenv("APP_ENV") == "testing"
# 修改系统路径
monkeypatch.syspath_prepend("/custom/path")
assert "/custom/path" in sys.path
# 修改函数行为
def mock_add(a, b):
return 42
monkeypatch.setattr("__main__.add", mock_add)
assert add(2, 3) == 42
# ======================
# 17. 测试执行顺序控制
# ======================
@pytest.mark.run(order=1)
def test_first():
"""第一个执行的测试"""
logger.info("首先执行")
assert True
@pytest.mark.run(order=3)
def test_third():
"""第三个执行的测试"""
logger.info("第三执行")
assert True
@pytest.mark.run(order=2)
def test_second():
"""第二个执行的测试"""
logger.info("第二执行")
assert True
# ======================
# 18. 测试依赖管理
# ======================
@pytest.mark.dependency()
def test_service_available():
"""测试服务可用性"""
# 模拟服务检查
assert True
@pytest.mark.dependency(depends=["test_service_available"])
def test_api_call():
"""依赖服务的API调用测试"""
# 只有服务可用时才执行
assert True
# ======================
# 19. 测试报告增强
# ======================
def test_report_enhancement():
"""测试报告增强功能"""
# 添加额外信息到报告
pytest_html = pytest.config.pluginmanager.getplugin("html")
if pytest_html:
extra = pytest_html.extras
# 添加文本
extra.append(extra.text("额外的文本信息"))
# 添加URL
extra.append(extra.url("https://example.com"))
# 添加图片
# extra.append(extra.image("screenshot.png"))
assert True
# ======================
# 20. 工厂夹具模式
# ======================
@pytest.fixture
def user_factory():
"""工厂夹具 - 创建用户"""
def _create_user(username, role="user"):
return create_user(username, role)
return _create_user
def test_user_factory(user_factory):
"""测试工厂夹具"""
admin = user_factory("factory_admin", "admin")
user = user_factory("factory_user")
assert admin["role"] == "admin"
assert user["role"] == "user"
如何运行这些测试
1. 基础运行
bash
# 运行所有测试
pytest test_pytest_features.py
# 显示详细信息
pytest -v test_pytest_features.py
# 显示打印输出
pytest -s test_pytest_features.py
2. 选择性运行
bash
# 运行特定标记的测试
pytest -m "slow or integration" test_pytest_features.py
# 运行包含特定关键字的测试
pytest -k "addition" test_pytest_features.py
# 运行特定类
pytest test_pytest_features.py::TestMathOperations
# 运行特定方法
pytest test_pytest_features.py::test_parametrized_addition
3. 高级运行选项
bash
# 并发运行(需要pytest-xdist)
pytest -n 4 test_pytest_features.py
# 遇到第一个错误停止
pytest -x test_pytest_features.py
# 允许最多2个失败
pytest --maxfail=2 test_pytest_features.py
4. 生成报告
bash
# 生成HTML报告
pytest --html=report.html test_pytest_features.py
# 生成Allure报告
pytest --alluredir=allure-results test_pytest_features.py
allure serve allure-results
学习要点总结
通过这套测试用例,您可以学习到:
- 基础测试结构:函数式测试和类测试
- 参数化测试:使用不同数据集运行相同测试逻辑
- 夹具系统:资源管理和依赖注入
- 标记系统:分组、跳过和预期失败测试
- 异常测试:验证代码是否按预期抛出异常
- 临时文件处理 :使用
tmp_path
夹具 - 输出捕获:捕获 stdout/stderr 和日志
- 并发测试:使用 pytest-xdist 插件
- 报告增强:Allure 报告集成
- 猴子补丁:动态修改测试环境
- 测试顺序控制:管理测试执行顺序
- 测试依赖:指定测试之间的依赖关系
- 工厂夹具:创建灵活的对象实例
- 配置管理:使用 pytest.ini
- 覆盖率集成:使用 pytest-cov 插件
每个测试用例都包含详细注释,解释了其目的和使用的 pytest 功能。通过运行这些测试并查看结果,您将能够全面掌握 pytest 的核心功能和高级特性。