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 自动处理!

七、最佳实践总结

该做的:

  1. 有意义的命名database_connection而不是 db
  2. 合理的作用域:根据资源开销选择适当的作用域
  3. 模块化设计:将复杂 Fixture 拆分为简单 Fixture 组合
  4. 使用 conftest.py:共享 Fixture 到整个项目

避免的:

  1. 过度复杂的 Fixture:一个 Fixture 做太多事情
  2. 隐式依赖:明确声明 Fixture 依赖关系
  3. 全局状态修改:避免在 Fixture 中修改全局状态

🔧 选择指南:

场景 推荐方案 示例
数据库连接 scope="session" 整个测试会话共用连接
测试数据 scope="function" 每个测试独立数据
昂贵资源 scope="module/session" 浏览器、Docker 容器
简单数据 直接返回数据 配置、常量

总结

pytest Fixture 的核心价值

  • 代码复用:消除重复的设置和清理代码
  • 依赖管理:清晰声明测试依赖关系
  • 资源管理:自动管理测试生命周期
  • 可读性:使测试用例更专注业务逻辑
  • 可维护性:集中管理测试环境配置

掌握了 Fixture,你就掌握了 pytest 最强大的功能!这是编写可维护、可扩展测试代码的关键。

posted @ 2025-11-30 19:24  GDms  阅读(0)  评论(0)    收藏  举报