python自动化测试:pytest(一).md
Python基础
测试常用库
好的!这些是 Python 自动化测试中最常用的文件操作库。我来为你详细讲解每个库的常用操作。
一、JSON 文件操作
1. 读取 JSON 文件
import json
# 方法1:读取文件
with open('data/users.json', 'r', encoding='utf-8') as f:
data = json.load(f) # 直接解析为 Python 对象
print(data) # 如果是列表或字典,可以直接使用
# 方法2:读取字符串
json_str = '{"name": "张三", "age": 25}'
data = json.loads(json_str) # 从字符串解析
print(data['name']) # 输出: 张三
2. 写入 JSON 文件
import json
# 要保存的数据
users = [
{"id": 1, "name": "张三", "email": "zhang@example.com"},
{"id": 2, "name": "李四", "email": "li@example.com"}
]
# 方法1:写入文件(格式化,易于阅读)
with open('output/users.json', 'w', encoding='utf-8') as f:
json.dump(users, f, ensure_ascii=False, indent=2)
# ensure_ascii=False: 支持中文
# indent=2: 格式化缩进
# 方法2:转换为字符串
json_str = json.dumps(users, ensure_ascii=False, indent=2)
print(json_str)
3. 自动化测试中的实际应用
import json
import pytest
class TestUserAPI:
def setup_method(self):
# 从 JSON 文件加载测试数据
with open('test_data/users.json', 'r', encoding='utf-8') as f:
self.test_users = json.load(f)
def test_user_creation(self):
"""使用 JSON 数据驱动测试"""
for user_data in self.test_users:
# 模拟 API 调用
response = create_user(user_data)
assert response['success'] == True
assert 'id' in response
def save_test_result(self, result_data):
"""保存测试结果到 JSON"""
with open('test_results/result.json', 'w', encoding='utf-8') as f:
json.dump(result_data, f, ensure_ascii=False, indent=2)
# 使用示例
if __name__ == "__main__":
test = TestUserAPI()
test.setup_method()
test.test_user_creation()
二、CSV 文件操作
1. 读取 CSV 文件
import csv
from typing import List, Dict
# 方法1:读取为字典列表(推荐)
def read_csv_as_dict(filename: str) -> List[Dict]:
with open(filename, 'r', encoding='utf-8-sig') as f: # utf-8-sig 处理 BOM
reader = csv.DictReader(f)
return list(reader)
# 方法2:读取为列表的列表
def read_csv_as_list(filename: str) -> List[List]:
with open(filename, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
return list(reader)
# 使用示例
users = read_csv_as_dict('data/users.csv')
for user in users:
print(f"姓名: {user['name']}, 年龄: {user['age']}")
2. 写入 CSV 文件
import csv
def write_to_csv(data: List[Dict], filename: str):
if not data:
return
# 获取表头(使用第一个字典的键)
fieldnames = data[0].keys()
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader() # 写入表头
writer.writerows(data) # 写入所有行
# 测试数据
test_results = [
{'test_case': '登录测试', 'status': 'PASS', 'duration': '1.2s'},
{'test_case': '注册测试', 'status': 'FAIL', 'duration': '2.1s'},
{'test_case': '支付测试', 'status': 'PASS', 'duration': '3.5s'}
]
write_to_csv(test_results, 'test_results.csv')
3. CSV 在数据驱动测试中的应用
import csv
import pytest
def load_test_data_from_csv(csv_file):
"""从 CSV 加载参数化测试数据"""
test_cases = []
with open(csv_file, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
for row in reader:
test_cases.append(row)
return test_cases
# 测试数据文件:login_test_data.csv
# username,password,expected_status,scenario
# admin,admin123,200,正确密码
# admin,wrongpass,401,错误密码
# ,admin123,400,用户名为空
class TestLogin:
@pytest.mark.parametrize("test_data", load_test_data_from_csv('data/login_test_data.csv'))
def test_login_parameters(self, test_data):
"""使用 CSV 数据驱动登录测试"""
username = test_data['username']
password = test_data['password']
expected_status = int(test_data['expected_status'])
# 调用登录接口
result = login(username, password)
# 断言
assert result['status_code'] == expected_status, \
f"场景失败: {test_data['scenario']}"
三、YAML 文件操作
1. 安装和导入
# 安装 PyYAML
pip install pyyaml
import yaml
# 安全加载(推荐)
def load_yaml_safe(filename: str):
with open(filename, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
2. 读取和写入 YAML
import yaml
# 配置文件示例:config.yaml
# database:
# host: localhost
# port: 3306
# username: testuser
# password: testpass
#
# testing:
# base_url: https://api.example.com
# timeout: 30
class ConfigManager:
def __init__(self, config_file='config.yaml'):
self.config_file = config_file
self.config = self.load_config()
def load_config(self):
"""加载 YAML 配置"""
with open(self.config_file, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def get(self, key, default=None):
"""获取配置值"""
keys = key.split('.')
value = self.config
for k in keys:
value = value.get(k, {})
return value if value != {} else default
def update_config(self, new_config):
"""更新配置并保存"""
self.config.update(new_config)
self.save_config()
def save_config(self):
"""保存配置到文件"""
with open(self.config_file, 'w', encoding='utf-8') as f:
yaml.dump(self.config, f, default_flow_style=False, allow_unicode=True)
# 使用示例
config = ConfigManager()
print(config.get('database.host')) # 输出: localhost
print(config.get('testing.timeout')) # 输出: 30
3. 测试配置管理实战
import yaml
import pytest
# 环境配置:environments.yaml
# dev:
# base_url: "https://dev-api.example.com"
# username: "test_dev"
# password: "dev123"
#
# test:
# base_url: "https://test-api.example.com"
# username: "test_user"
# password: "test123"
def load_environment_config(env='test'):
"""加载环境配置"""
with open('config/environments.yaml', 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f)
return all_configs.get(env, {})
@pytest.fixture
def api_config(request):
"""Pytest fixture 提供配置"""
env = request.config.getoption("--env", default="test")
return load_environment_config(env)
def test_with_environment_config(api_config):
"""使用环境配置的测试"""
base_url = api_config['base_url']
# 使用配置进行测试...
print(f"测试环境: {base_url}")
四、Excel 文件操作(openpyxl)
1. 安装和基础操作
# 安装 openpyxl
pip install openpyxl
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill
import openpyxl
class ExcelManager:
def __init__(self, filename):
self.filename = filename
self.workbook = None
self.sheet = None
def create_workbook(self, sheet_name="Sheet1"):
"""创建新的 Excel 文件"""
self.workbook = Workbook()
self.sheet = self.workbook.active
self.sheet.title = sheet_name
return self.workbook
def load_workbook(self):
"""加载现有 Excel 文件"""
self.workbook = load_workbook(self.filename)
self.sheet = self.workbook.active
return self.workbook
2. 读取 Excel 数据
def read_excel_as_dict(self, start_row=2):
"""将 Excel 读取为字典列表(第一行为表头)"""
if not self.workbook:
self.load_workbook()
data = []
headers = []
# 读取表头
for col in range(1, self.sheet.max_column + 1):
header = self.sheet.cell(row=1, column=col).value
headers.append(header if header else f"Column{col}")
# 读取数据行
for row in range(start_row, self.sheet.max_row + 1):
row_data = {}
for col, header in enumerate(headers, 1):
cell_value = self.sheet.cell(row=row, column=col).value
row_data[header] = cell_value
# 跳过空行
if any(row_data.values()):
data.append(row_data)
return data
# 使用示例
excel_mgr = ExcelManager('test_data.xlsx')
excel_mgr.load_workbook()
test_cases = excel_mgr.read_excel_as_dict()
3. 写入 Excel 和格式化
def write_test_results(self, results):
"""写入测试结果到 Excel"""
if not self.workbook:
self.create_workbook("测试结果")
# 写入表头
headers = ["测试用例", "状态", "执行时间", "错误信息", "时间戳"]
for col, header in enumerate(headers, 1):
cell = self.sheet.cell(row=1, column=col)
cell.value = header
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color="FFFF00", end_color="FFFF00", fill_type="solid")
# 写入数据
for row, result in enumerate(results, 2):
self.sheet.cell(row=row, column=1).value = result.get('test_case', '')
self.sheet.cell(row=row, column=2).value = result.get('status', '')
self.sheet.cell(row=row, column=3).value = result.get('duration', '')
self.sheet.cell(row=row, column=4).value = result.get('error_message', '')
self.sheet.cell(row=row, column=5).value = result.get('timestamp', '')
# 根据状态设置颜色
status_cell = self.sheet.cell(row=row, column=2)
if result.get('status') == 'PASS':
status_cell.fill = PatternFill(start_color="00FF00", fill_type="solid")
else:
status_cell.fill = PatternFill(start_color="FF0000", fill_type="solid")
# 自动调整列宽
for column in self.sheet.columns:
max_length = 0
column_letter = column[0].column_letter
for cell in column:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
adjusted_width = (max_length + 2) * 1.2
self.sheet.column_dimensions[column_letter].width = adjusted_width
self.workbook.save(self.filename)
4. 完整测试报告生成示例
import openpyxl
from datetime import datetime
import pytest
class TestReportGenerator:
def __init__(self, report_file="test_report.xlsx"):
self.report_file = report_file
self.results = []
def add_result(self, test_case, status, duration, error_message=""):
"""添加测试结果"""
self.results.append({
'test_case': test_case,
'status': status,
'duration': duration,
'error_message': error_message,
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
})
def generate_report(self):
"""生成 Excel 测试报告"""
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "测试报告"
# 写入表头
headers = ["测试用例", "状态", "执行时间", "错误信息", "时间戳"]
for col, header in enumerate(headers, 1):
ws.cell(row=1, column=col, value=header).font = openpyxl.styles.Font(bold=True)
# 写入数据
for row, result in enumerate(self.results, 2):
ws.cell(row=row, column=1, value=result['test_case'])
status_cell = ws.cell(row=row, column=2, value=result['status'])
ws.cell(row=row, column=3, value=result['duration'])
ws.cell(row=row, column=4, value=result['error_message'])
ws.cell(row=row, column=5, value=result['timestamp'])
# 状态单元格着色
if result['status'] == 'PASS':
status_cell.fill = openpyxl.styles.PatternFill(
start_color="00FF00", fill_type="solid"
)
else:
status_cell.fill = openpyxl.styles.PatternFill(
start_color="FF0000", fill_type="solid"
)
# 保存文件
wb.save(self.report_file)
print(f"测试报告已生成: {self.report_file}")
# 在测试中使用
@pytest.fixture(scope="session")
def report_generator():
generator = TestReportGenerator()
yield generator
generator.generate_report() # 测试结束后生成报告
def test_example(report_generator):
try:
# 执行测试逻辑
result = some_test_function()
report_generator.add_result(
test_case="示例测试",
status="PASS",
duration="1.2s"
)
assert result is True
except Exception as e:
report_generator.add_result(
test_case="示例测试",
status="FAIL",
duration="0.5s",
error_message=str(e)
)
raise
五、综合实战:测试数据管理框架
import json
import csv
import yaml
from openpyxl import load_workbook
from typing import Dict, List, Any
class TestDataManager:
"""统一的测试数据管理器"""
@staticmethod
def load_test_data(file_path: str) -> Any:
"""根据文件扩展名自动选择加载方式"""
if file_path.endswith('.json'):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
elif file_path.endswith('.yaml') or file_path.endswith('.yml'):
with open(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
elif file_path.endswith('.csv'):
with open(file_path, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
return list(reader)
elif file_path.endswith(('.xlsx', '.xls')):
wb = load_workbook(file_path)
ws = wb.active
data = []
headers = [cell.value for cell in ws[1]]
for row in ws.iter_rows(min_row=2, values_only=True):
data.append(dict(zip(headers, row)))
return data
else:
raise ValueError(f"不支持的文件格式: {file_path}")
@staticmethod
def save_test_data(data: Any, file_path: str):
"""保存测试数据到文件"""
if file_path.endswith('.json'):
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
elif file_path.endswith('.yaml') or file_path.endswith('.yml'):
with open(file_path, 'w', encoding='utf-8') as f:
yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
elif file_path.endswith('.csv') and data:
with open(file_path, 'w', newline='', encoding='utf-8-sig') as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
else:
raise ValueError(f"不支持的保存格式: {file_path}")
# 使用示例
if __name__ == "__main__":
# 加载各种格式的测试数据
json_data = TestDataManager.load_test_data('data/users.json')
csv_data = TestDataManager.load_test_data('data/test_cases.csv')
yaml_config = TestDataManager.load_test_data('config.yaml')
print("JSON 数据:", json_data)
print("CSV 数据:", csv_data)
print("YAML 配置:", yaml_config)
总结对比
| 格式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| JSON | API 测试、配置数据 | 结构清晰、通用性强 | 不支持注释 |
| CSV | 数据驱动测试、大量数据 | 简单、Excel可打开 | 无数据类型 |
| YAML | 配置文件、复杂结构 | 可读性好、支持注释 | 格式要求严格 |
| Excel | 测试报告、业务数据 | 可视化好、功能强大 | 依赖库较大 |
选择建议:
- 配置管理:使用 YAML(可读性最好)
- API 测试数据:使用 JSON(与 API 格式一致)
- 参数化测试:使用 CSV(简单高效)
- 测试报告:使用 Excel(美观易分享)
掌握这些文件操作,你就能高效处理各种测试数据和报告了!
Pytest测试框架
好的!这是一份完整的 pytest 学习路径,从入门到高级,包含实际项目应用。
一、pytest 学习路线总览
graph TD
A[pytest学习路径] --> B[基础入门]
A --> C[核心功能]
A --> D[高级特性]
A --> E[项目实战]
A --> F[最佳实践]
B --> B1[安装和运行]
B --> B2[编写测试用例]
B --> B3[断言使用]
C --> C1[Fixture装置]
C --> C2[参数化]
C --> C3[标记和过滤]
D --> D1[插件系统]
D --> D2[钩子函数]
D --> D3[自定义配置]
E --> E1[API测试]
E --> E2[Web UI测试]
E --> E3[数据库测试]
F --> F1[项目结构]
F --> F2[CI/CD集成]
F --> F3[性能优化]
二、阶段1:基础入门(1-2天)
1. 安装和第一个测试
# 安装 pytest
pip install pytest
# 验证安装
pytest --version
创建第一个测试文件 test_basic.py:
# test_basic.py
def add(a, b):
return a + b
def test_add():
"""测试加法函数"""
assert add(2, 3) == 5
assert add(-1, 1) == 0
assert add(0, 0) == 0
def test_string_concatenation():
"""测试字符串拼接"""
result = "Hello" + " " + "World"
assert result == "Hello World"
运行测试:
# 运行所有测试
pytest
# 运行特定文件
pytest test_basic.py
# 显示详细信息
pytest -v
# 显示打印输出
pytest -s
2. 断言和测试发现
pytest 的智能断言:
def test_assertions():
"""各种断言示例"""
# 相等断言
assert 1 + 1 == 2
# 包含断言
assert "hello" in "hello world"
# 类型断言
assert isinstance("hello", str)
# 异常断言
with pytest.raises(ValueError):
int("invalid")
# 近似相等断言
assert 0.1 + 0.2 == pytest.approx(0.3)
测试发现规则(pytest的探测规则):
- 文件命名:
test_*.py或*_test.py - 函数命名:
test_* - 类命名:
Test*
三、阶段2:核心功能(3-5天)
1. Fixture 装置(pytest 的灵魂)
pytest Fixture(装置) 是 pytest 测试框架的核心功能,也是它比 unittest 等框架更强大的关键特性。
一、什么是 Fixture?
简单定义
Fixture 是一个可重用的设置和清理函数,用于为测试用例提供依赖资源和测试环境。
生动比喻
把 Fixture 想象成餐厅的后厨准备:
| 场景 | 比喻 | 对应 Fixture |
|---|---|---|
| 顾客点餐前 | 厨师准备食材、厨具 | setup 准备阶段 |
| 顾客用餐 | 顾客享受美食 | 测试用例执行 |
| 顾客离开后 | 服务员清理餐桌 | teardown 清理阶段 |
二、为什么需要 Fixture?
没有 Fixture 的问题(传统方式)
# 传统测试方式 - 重复代码多
def test_user_login():
# 每个测试都要重复这些准备代码
db = connect_database() # 连接数据库
user = create_test_user(db) # 创建测试用户
client = create_http_client() # 创建 HTTP 客户端
# 实际测试逻辑
result = client.login(user.username, "password123")
assert result.success == True
# 清理工作也要重复
db.delete_user(user.id)
db.close()
client.logout()
def test_user_profile():
# 又重复一遍!
db = connect_database()
user = create_test_user(db)
client = create_http_client()
# 实际测试逻辑
profile = client.get_profile(user.id)
assert profile is not None
# 重复清理
db.delete_user(user.id)
db.close()
client.logout()
使用 Fixture 的优雅解决方案
import pytest
@pytest.fixture
def database():
"""数据库连接 fixture"""
db = connect_database()
yield db # 测试使用
db.close() # 测试后清理
@pytest.fixture
def test_user(database):
"""测试用户 fixture"""
user = database.create_user("testuser", "test@example.com")
yield user
database.delete_user(user.id)
@pytest.fixture
def api_client():
"""API 客户端 fixture"""
client = create_http_client()
yield client
client.logout()
def test_user_login(database, test_user, api_client):
"""使用 fixture - 简洁清晰!"""
result = api_client.login(test_user.username, "password123")
assert result.success == True
def test_user_profile(test_user, api_client):
"""复用 fixture - 无需重复代码!"""
profile = api_client.get_profile(test_user.id)
assert profile is not None
三、Fixture 的核心特性
1. 基本语法
import pytest
@pytest.fixture
def simple_fixture():
"""最简单的 fixture"""
print(">>> 准备阶段")
data = "我是fixture提供的数据"
yield data # 测试用例从这里获取数据
print(">>> 清理阶段")
def test_example(simple_fixture):
"""使用 fixture 的测试"""
print(f"测试使用: {simple_fixture}")
assert simple_fixture == "我是fixture提供的数据"
# 运行输出:
# >>> 准备阶段
# 测试使用: 我是fixture提供的数据
# >>> 清理阶段
2. Fixture 的作用域(Scope)
控制 Fixture 的创建和销毁频率:
import pytest
@pytest.fixture(scope="function") # 默认:每个测试函数执行一次
def function_scope():
return "每个测试函数都会重新创建"
@pytest.fixture(scope="class") # 每个测试类执行一次
def class_scope():
return "每个测试类只创建一次"
@pytest.fixture(scope="module") # 每个模块执行一次
def module_scope():
return "每个模块只创建一次"
@pytest.fixture(scope="session") # 整个测试会话执行一次
def session_scope():
return "整个pytest运行期间只创建一次"
作用域对比示例:
class TestExample:
def test_one(self, function_scope, class_scope, module_scope, session_scope):
print(f"Test1 - 函数级: {function_scope}")
print(f"Test1 - 类级: {class_scope}")
print(f"Test1 - 模块级: {module_scope}")
print(f"Test1 - 会话级: {session_scope}")
def test_two(self, function_scope, class_scope, module_scope, session_scope):
print(f"Test2 - 函数级: {function_scope}") # 新的实例!
print(f"Test2 - 类级: {class_scope}") # 相同实例
print(f"Test2 - 模块级: {module_scope}") # 相同实例
print(f"Test2 - 会话级: {session_scope}") # 相同实例
3. Fixture 依赖关系
Fixture 可以依赖其他 Fixture,形成依赖链:
import pytest
@pytest.fixture
def database():
"""基础数据库连接"""
print("连接数据库")
db = "Database Connection"
yield db
print("关闭数据库")
@pytest.fixture
def test_user(database): # 依赖 database fixture
"""创建测试用户(需要数据库)"""
print(f"使用 {database} 创建用户")
user = {"id": 1, "name": "测试用户"}
yield user
print("删除测试用户")
@pytest.fixture
def authenticated_client(database, test_user): # 依赖多个 fixture
"""已认证的客户端(需要数据库和用户)"""
print(f"使用 {test_user} 进行认证")
client = f"认证客户端(用户: {test_user['name']})"
yield client
print("客户端登出")
def test_with_dependencies(authenticated_client):
"""使用有依赖关系的 fixture"""
print(f"执行测试,使用: {authenticated_client}")
assert "认证客户端" in authenticated_client
执行顺序:
连接数据库
使用 Database Connection 创建用户
使用 {'id': 1, 'name': '测试用户'} 进行认证
执行测试,使用: 认证客户端(用户: 测试用户)
客户端登出
删除测试用户
关闭数据库
四、Fixture 的实际应用场景
1. 测试数据准备
import pytest
@pytest.fixture
def sample_product():
"""示例商品数据"""
return {
"id": 123,
"name": "测试商品",
"price": 99.99,
"stock": 50
}
@pytest.fixture
def sample_order(sample_product): # 依赖商品 fixture
"""示例订单数据"""
return {
"order_id": 1001,
"products": [sample_product],
"total_amount": sample_product["price"] * 2,
"quantity": 2
}
def test_order_creation(sample_order):
"""测试订单创建"""
assert sample_order["order_id"] == 1001
assert len(sample_order["products"]) == 1
assert sample_order["total_amount"] == 199.98
2. 外部资源管理
import pytest
import tempfile
import os
@pytest.fixture
def temp_config_file():
"""创建临时配置文件"""
# 创建临时文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
json.dump({"debug": True, "timeout": 30}, f)
temp_path = f.name
yield temp_path # 提供给测试使用
# 测试完成后清理
os.unlink(temp_path)
print(f"已删除临时文件: {temp_path}")
@pytest.fixture
def database_connection():
"""数据库连接 fixture"""
import sqlite3
conn = sqlite3.connect(":memory:") # 内存数据库
conn.execute("""
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
""")
yield conn
conn.close()
def test_with_temp_file(temp_config_file, database_connection):
"""使用临时文件和数据库的测试"""
# 使用临时配置文件
with open(temp_config_file, 'r') as f:
config = json.load(f)
assert config["debug"] is True
# 使用内存数据库
database_connection.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("张三", "zhang@example.com")
)
result = database_connection.execute("SELECT * FROM users").fetchone()
assert result[1] == "张三"
3. 模拟(Mock)对象
import pytest
from unittest.mock import Mock, patch
@pytest.fixture
def mock_http_client():
"""模拟 HTTP 客户端"""
client = Mock()
client.get.return_value = Mock(status_code=200, json=lambda: {"data": "test"})
client.post.return_value = Mock(status_code=201, json=lambda: {"id": 123})
return client
@pytest.fixture
def mock_database():
"""模拟数据库"""
db = Mock()
db.query.return_value = [{"id": 1, "name": "测试用户"}]
db.insert.return_value = True
return db
def test_with_mocks(mock_http_client, mock_database):
"""使用模拟对象的测试"""
# 测试业务逻辑,不依赖真实外部服务
response = mock_http_client.get("/api/users")
assert response.status_code == 200
users = mock_database.query("SELECT * FROM users")
assert len(users) == 1
assert users[0]["name"] == "测试用户"
五、高级 Fixture 技巧
1. 参数化 Fixture
import pytest
@pytest.fixture(params=["chrome", "firefox", "safari"])
def browser(request): # request 是内置 fixture
"""参数化浏览器 fixture"""
browser_name = request.param
print(f"\n启动浏览器: {browser_name}")
# 模拟启动不同浏览器
browser = f"{browser_name}_browser"
yield browser
print(f"关闭浏览器: {browser_name}")
def test_with_browser(browser):
"""对每个浏览器参数运行一次"""
print(f"使用浏览器执行测试: {browser}")
assert "browser" in browser
# 运行结果:
# 启动浏览器: chrome
# 使用浏览器执行测试: chrome_browser
# 关闭浏览器: chrome
# 启动浏览器: firefox
# 使用浏览器执行测试: firefox_browser
# 关闭浏览器: firefox
# ...(safari 同样执行)
2. 自动使用 Fixture(autouse)
如果有多个autouse,顺序是按照function_name的首字母顺序来执行的
import pytest
@pytest.fixture(autouse=True) # 自动使用,无需在参数中声明
def global_setup_teardown():
"""自动在每个测试前后执行"""
print("\n=== 测试开始 ===")
yield
print("=== 测试结束 ===\n")
@pytest.fixture
def log_test_name(request): # request 可以获取测试信息
"""记录测试名称"""
print(f"执行测试: {request.function.__name__}")
yield
print(f"完成测试: {request.function.__name__}")
def test_one():
"""测试一"""
print("测试一的主体逻辑")
assert True
def test_two():
"""测试二"""
print("测试二的主体逻辑")
assert True
# 输出:
# === 测试开始 ===
# 执行测试: test_one
# 测试一的主体逻辑
# 完成测试: test_one
# === 测试结束 ===
#
# === 测试开始 ===
# 执行测试: test_two
# 测试二的主体逻辑
# 完成测试: test_two
# === 测试结束 ===
3. Fixture 最终化器(addfinalizer)
addfinalizer 是 pytest Fixture 中用于注册清理函数的另一种方式,与 yield功能类似,但更灵活,特别是在需要多个清理步骤或条件清理的场景下。
一、基本语法对比
1. 使用 yield(常见方式)
import pytest
@pytest.fixture
def resource_with_yield():
print(">>> 准备资源")
resource = "我的资源"
yield resource # 测试使用这里
print(">>> 清理资源") # 测试后执行
def test_example(resource_with_yield):
print(f"使用资源: {resource_with_yield}")
2. 使用 addfinalizer(等效方式)
import pytest
@pytest.fixture
def resource_with_finalizer(request): # 必须接收 request 参数
print(">>> 准备资源")
resource = "我的资源"
def cleanup():
print(">>> 清理资源")
request.addfinalizer(cleanup) # 注册清理函数
return resource # 直接返回资源
def test_example(resource_with_finalizer):
print(f"使用资源: {resource_with_finalizer}")
两种方式输出相同:
>>> 准备资源
使用资源: 我的资源
>>> 清理资源
二、为什么需要 addfinalizer?yield 的局限性
yield 的问题:无法处理多个清理步骤
import pytest
@pytest.fixture
def complex_resource_with_yield():
"""复杂资源 - yield 方式(有问题)"""
print("1. 初始化数据库")
db = "数据库连接"
print("2. 创建测试数据")
test_data = "测试数据"
yield db, test_data # 返回多个资源
# 问题:如果这里出现异常,后面的清理不会执行!
print("3. 删除测试数据") # 可能因为异常跳过
print("4. 关闭数据库连接") # 这个可能不会执行
def test_complex_yield():
db, data = complex_resource_with_yield()
print(f"使用: {db}, {data}")
# 如果 "删除测试数据" 步骤抛出异常,"关闭数据库" 不会执行!
addfinalizer 的解决方案:确保所有清理都会执行
import pytest
@pytest.fixture
def complex_resource_with_finalizer(request):
"""复杂资源 - addfinalizer 方式(更安全)"""
print("1. 初始化数据库")
db = "数据库连接"
print("2. 创建测试数据")
test_data = "测试数据"
# 注册多个清理函数(逆序执行)
def cleanup_database():
print("4. 关闭数据库连接") # 最后执行
def cleanup_test_data():
print("3. 删除测试数据") # 先执行
request.addfinalizer(cleanup_database) # 第二个注册,第一个执行
request.addfinalizer(cleanup_test_data) # 第一个注册,第二个执行
return db, test_data
def test_complex_finalizer(complex_resource_with_finalizer):
db, data = complex_resource_with_finalizer
print(f"使用: {db}, {data}")
# 输出(即使清理步骤有异常,其他清理仍会执行):
# 1. 初始化数据库
# 2. 创建测试数据
# 使用: 数据库连接, 测试数据
# 3. 删除测试数据 ← 即使这里异常...
# 4. 关闭数据库连接 ← ...这个仍会执行!
三、addfinalizer 的核心优势
优势1:多个清理步骤,确保执行
import pytest
@pytest.fixture
def multi_step_cleanup(request):
"""多步骤清理示例"""
resources = []
# 步骤1:创建临时文件
temp_file = "/tmp/test1.txt"
resources.append(f"文件: {temp_file}")
print(f"创建: {temp_file}")
# 步骤2:建立网络连接
network_conn = "网络连接"
resources.append(f"连接: {network_conn}")
print(f"建立: {network_conn}")
# 步骤3:启动后台进程
background_process = "后台进程"
resources.append(f"进程: {background_process}")
print(f"启动: {background_process}")
# 注册多个清理函数(按注册顺序的逆序执行)
def cleanup_step3():
print(f"停止: {background_process}")
# 即使这里抛出异常,下面的清理仍会执行!
def cleanup_step2():
print(f"关闭: {network_conn}")
def cleanup_step1():
print(f"删除: {temp_file}")
request.addfinalizer(cleanup_step3) # 第三注册,第一执行
request.addfinalizer(cleanup_step2) # 第二注册,第二执行
request.addfinalizer(cleanup_step1) # 第一注册,第三执行
return resources
def test_multi_cleanup(multi_step_cleanup):
print("执行测试...")
print(f"使用资源: {multi_step_cleanup}")
# 输出:
# 创建: /tmp/test1.txt
# 建立: 网络连接
# 启动: 后台进程
# 执行测试...
# 使用资源: ['文件: /tmp/test1.txt', '连接: 网络连接', '进程: 后台进程']
# 停止: 后台进程 ← 逆序执行!
# 关闭: 网络连接
# 删除: /tmp/test1.txt
优势2:条件性清理
import pytest
@pytest.fixture
def conditional_cleanup(request):
"""根据测试结果决定如何清理"""
test_data = {"id": 1, "name": "测试数据"}
should_cleanup_completely = True # 默认完全清理
def cleanup():
if should_cleanup_completely:
print("执行完整清理: 删除所有数据")
else:
print("执行部分清理: 保留测试数据")
# 可以在测试中修改清理行为
request.addfinalizer(cleanup)
# 返回一个可以修改清理行为的函数
def set_cleanup_strategy(complete=True):
nonlocal should_cleanup_completely
should_cleanup_completely = complete
return test_data, set_cleanup_strategy
def test_conditional_cleanup(conditional_cleanup):
test_data, set_cleanup = conditional_cleanup
print(f"使用数据: {test_data}")
# 根据测试结果决定清理策略
if some_condition: # 某些条件下保留数据
set_cleanup(complete=False) # 设置为部分清理
else:
set_cleanup(complete=True) # 设置为完全清理
优势3:异常安全的资源管理
import pytest
class DatabaseConnection:
def __init__(self, name):
self.name = name
self.connected = True
print(f"连接数据库: {name}")
def close(self):
if self.connected:
print(f"安全关闭: {self.name}")
self.connected = False
else:
print(f"已经关闭: {self.name}")
@pytest.fixture
def safe_database(request):
"""异常安全的数据库连接"""
db = DatabaseConnection("主数据库")
def safe_cleanup():
# 即使多次调用,也是安全的
db.close()
request.addfinalizer(safe_cleanup)
return db
def test_safe_operations(safe_database):
print("执行数据库操作...")
# 即使这里抛出异常,清理函数仍会执行
raise Exception("模拟测试失败!")
# 输出:
# 连接数据库: 主数据库
# 执行数据库操作...
# 安全关闭: 主数据库 ← 即使测试失败也会执行!
四、实际应用场景
场景1:测试事务回滚
import pytest
import sqlite3
@pytest.fixture
def database_transaction(request):
"""数据库事务 fixture,测试后自动回滚"""
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
# 开始事务
conn.execute("BEGIN TRANSACTION")
def rollback_and_close():
print("回滚事务并关闭连接")
conn.execute("ROLLBACK")
conn.close()
request.addfinalizer(rollback_and_close)
return conn
def test_database_operations(database_transaction):
"""数据库操作测试,自动回滚"""
db = database_transaction
db.execute("INSERT INTO users VALUES (1, '测试用户')")
result = db.execute("SELECT * FROM users").fetchall()
assert len(result) == 1 # 测试期间数据存在
# 测试结束后自动回滚,表恢复为空
场景2:复杂的多资源管理
import pytest
import tempfile
import os
@pytest.fixture
def complex_test_environment(request):
"""复杂的测试环境设置"""
resources = {}
# 1. 创建临时目录
temp_dir = tempfile.mkdtemp()
resources['temp_dir'] = temp_dir
print(f"创建临时目录: {temp_dir}")
# 2. 在目录中创建多个文件
test_files = []
for i in range(3):
file_path = os.path.join(temp_dir, f"test_{i}.txt")
with open(file_path, 'w') as f:
f.write(f"测试数据 {i}")
test_files.append(file_path)
resources['test_files'] = test_files
print(f"创建测试文件: {test_files}")
# 3. 启动模拟服务
mock_service = "模拟服务进程"
resources['service'] = mock_service
print(f"启动服务: {mock_service}")
# 注册多个清理函数(逆序!)
def stop_service():
print(f"停止服务: {resources['service']}")
def delete_files():
for file_path in resources['test_files']:
if os.path.exists(file_path):
os.unlink(file_path)
print(f"删除文件: {file_path}")
def remove_temp_dir():
if os.path.exists(resources['temp_dir']):
os.rmdir(resources['temp_dir'])
print(f"删除目录: {resources['temp_dir']}")
request.addfinalizer(stop_service) # 最后注册,最先执行
request.addfinalizer(delete_files) # 第二注册,第二执行
request.addfinalizer(remove_temp_dir) # 最先注册,最后执行
return resources
def test_complex_environment(complex_test_environment):
print("在复杂环境中执行测试...")
assert len(complex_test_environment['test_files']) == 3
场景3:性能测试的精确计时
import pytest
import time
@pytest.fixture
def performance_timer(request):
"""性能测试计时器"""
start_time = time.time()
operations = []
def record_operation(name):
operations.append((name, time.time() - start_time))
def generate_report():
print("\n=== 性能测试报告 ===")
for op_name, duration in operations:
print(f"{op_name}: {duration:.3f}秒")
total_time = time.time() - start_time
print(f"总耗时: {total_time:.3f}秒")
# 测试结束后自动生成报告
request.addfinalizer(generate_report)
return record_operation
def test_performance(performance_timer):
timer = performance_timer
timer("开始测试")
time.sleep(0.1)
timer("步骤1完成")
time.sleep(0.2)
timer("步骤2完成")
time.sleep(0.15)
timer("测试完成")
# 输出:
# 开始测试
# 步骤1完成
# 步骤2完成
# 测试完成
#
# === 性能测试报告 ===
# 开始测试: 0.000秒
# 步骤1完成: 0.101秒
# 步骤2完成: 0.302秒
# 测试完成: 0.453秒
# 总耗时: 0.453秒
五、addfinalizer vs yield 选择指南
使用 yield的情况(推荐用于简单场景) ✅
# 适合 yield 的场景:
@pytest.fixture
def simple_fixture():
resource = setup_resource()
yield resource # 简单清晰
cleanup_resource() # 单一清理步骤
使用 addfinalizer的情况(复杂场景) 🔧
# 适合 addfinalizer 的场景:
@pytest.fixture
def complex_fixture(request):
setup_multiple_resources()
# 需要多个清理步骤
request.addfinalizer(cleanup_step3)
request.addfinalizer(cleanup_step2)
request.addfinalizer(cleanup_step1) # 逆序执行!
# 或需要条件清理
def conditional_cleanup():
if condition:
full_cleanup()
else:
partial_cleanup()
request.addfinalizer(conditional_cleanup)
return resources
决策流程图
graph TD
A[选择 Fixture 清理方式] --> B{清理需求}
B -->|单一清理步骤| C[使用 yield<br/>简单直观]
B -->|多个清理步骤| D[使用 addfinalizer<br/>确保所有清理执行]
B -->|条件性清理| E[使用 addfinalizer<br/>灵活控制]
B -->|异常安全重要| F[使用 addfinalizer<br/>异常安全]
C --> G[完成]
D --> G
E --> G
F --> G
六、最佳实践总结
✅ 使用 addfinalizer 当:
- 需要多个清理步骤,且必须全部执行
- 清理顺序重要(逆序注册)
- 需要条件性清理逻辑
- 异常安全是关键要求
- 清理逻辑复杂,需要函数封装
✅ 使用 yield 当:
- 只有单一清理步骤
- 代码要求简洁直观
- 不需要复杂清理逻辑
- 团队更熟悉 yield 语法
🔧 实际建议:
# 大多数情况用 yield(更 Pythonic)
@pytest.fixture
def database():
db = connect_db()
yield db
db.close() # 简单明了
# 复杂情况用 addfinalizer
@pytest.fixture
def complex_environment(request):
setup_multiple_things()
# 明确的清理顺序控制
request.addfinalizer(cleanup_third)
request.addfinalizer(cleanup_second)
request.addfinalizer(cleanup_first)
return resources
总结:addfinalizer是 pytest 提供的一个更强大、更灵活的清理机制,虽然在简单场景下 yield更常用,但在需要精细控制清理过程时,addfinalizer是不可或缺的工具。
六、实战项目示例
完整的 API 测试 Fixture 配置
# conftest.py
import pytest
import requests
from typing import Dict, Any
@pytest.fixture(scope="session")
def api_config():
"""API 配置(会话级)"""
return {
"base_url": "https://api.example.com",
"timeout": 30,
"max_retries": 3
}
@pytest.fixture(scope="session")
def http_session():
"""HTTP 会话(会话级)"""
session = requests.Session()
session.headers.update({
"User-Agent": "pytest-api-tests/1.0",
"Accept": "application/json"
})
yield session
session.close()
@pytest.fixture
def api_client(api_config, http_session):
"""API 客户端"""
client = APIClient(api_config["base_url"], http_session)
yield client
client.cleanup()
@pytest.fixture
def authenticated_client(api_client):
"""已认证的客户端"""
api_client.login("testuser", "testpass")
yield api_client
api_client.logout()
@pytest.fixture
def test_user(authenticated_client):
"""测试用户"""
user_data = {
"name": "Fixture测试用户",
"email": "fixture@example.com"
}
user = authenticated_client.create_user(user_data)
yield user
authenticated_client.delete_user(user["id"])
# 测试用例
def test_user_workflow(authenticated_client, test_user):
"""完整的用户工作流测试"""
# 查询用户
user = authenticated_client.get_user(test_user["id"])
assert user["name"] == "Fixture测试用户"
# 更新用户
updated_user = authenticated_client.update_user(
test_user["id"], {"name": "更新后的名称"}
)
assert updated_user["name"] == "更新后的名称"
# 注意:清理工作由 fixture 自动处理!
七、最佳实践总结
✅ 该做的:
- 有意义的命名:
database_connection而不是db - 合理的作用域:根据资源开销选择适当的作用域
- 模块化设计:将复杂 Fixture 拆分为简单 Fixture 组合
- 使用 conftest.py:共享 Fixture 到整个项目
❌ 避免的:
- 过度复杂的 Fixture:一个 Fixture 做太多事情
- 隐式依赖:明确声明 Fixture 依赖关系
- 全局状态修改:避免在 Fixture 中修改全局状态
🔧 选择指南:
| 场景 | 推荐方案 | 示例 |
|---|---|---|
| 数据库连接 | scope="session" |
整个测试会话共用连接 |
| 测试数据 | scope="function" |
每个测试独立数据 |
| 昂贵资源 | scope="module/session" |
浏览器、Docker 容器 |
| 简单数据 | 直接返回数据 | 配置、常量 |
总结
pytest Fixture 的核心价值:
- ✅ 代码复用:消除重复的设置和清理代码
- ✅ 依赖管理:清晰声明测试依赖关系
- ✅ 资源管理:自动管理测试生命周期
- ✅ 可读性:使测试用例更专注业务逻辑
- ✅ 可维护性:集中管理测试环境配置
掌握了 Fixture,你就掌握了 pytest 最强大的功能!这是编写可维护、可扩展测试代码的关键。

浙公网安备 33010602011771号