pytest_自动化测试4

6. pytest测试报告与覆盖率

6.1 pytest测试报告体系概述

pytest提供了丰富的测试报告功能,包括终端输出、HTML报告、JUnit XML报告等。

测试报告类型

pytest测试报告体系
终端报告
HTML报告
JUnit XML报告
覆盖率报告
自定义报告
简洁模式
详细模式
彩色输出
pytest-html
allure-pytest
pytest-reportlog
JUnit格式
CI/CD集成
测试历史
pytest-cov
coverage.py
HTML覆盖率
自定义钩子
自定义格式
第三方插件

基础测试报告示例
python 复制代码
# test_report_basic.py
import pytest

def test_passed():
    """
    通过的测试
    """
    assert 1 + 1 == 2

def test_failed():
    """
    失败的测试
    """
    assert 1 + 1 == 3

@pytest.mark.skip(reason="这个测试被跳过")
def test_skipped():
    """
    跳过的测试
    """
    assert True

@pytest.mark.xfail(reason="这个测试预期失败")
def test_xfailed():
    """
    预期失败的测试
    """
    assert 1 + 1 == 3

@pytest.mark.xfail(reason="这个测试预期失败")
def test_xpassed():
    """
    意外通过的测试
    """
    assert 1 + 1 == 2

6.2 pytest-html报告生成

pytest-html插件可以生成美观的HTML测试报告。

pytest-html基础使用
python 复制代码
# test_html_report.py
import pytest

# 安装:pip install pytest-html

def test_html_report_1():
    """
    HTML报告测试1
    """
    assert True

def test_html_report_2():
    """
    HTML报告测试2
    """
    assert 1 + 1 == 2

def test_html_report_3():
    """
    HTML报告测试3 - 失败测试
    """
    assert 1 + 1 == 3

def test_html_report_with_screenshot(request):
    """
    带截图的HTML报告测试
    注意:pytest-html 3.0+版本中添加截图的方式已改变
    """
    # 模拟截图功能
    screenshot_data = "base64_encoded_image_data"
    
    # 在pytest-html 3.0+中,使用pytest_html_extra钩子添加额外内容
    # 这里演示如何添加截图到报告
    # 实际使用时,需要在conftest.py中实现pytest_html_results_table_row钩子
    
    # 示例:在conftest.py中添加截图
    """
    # conftest.py
    @pytest.hookimpl(hookwrapper=True)
    def pytest_runtest_makereport(item, call):
        outcome = yield
        report = outcome.get_result()
        if report.when == "call" and hasattr(report, "extra"):
            # 添加截图到报告
            report.extra.append(pytest_html.extras.image(screenshot_data))
    """
    
    assert True
pytest-html配置选项
ini 复制代码
# pytest.ini
[pytest]
# HTML报告配置
htmlpath = reports/report.html
self_contained_html = true
python 复制代码
# conftest.py
import pytest
import time
import sys

@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    """
    自定义HTML报告内容
    注意:此示例展示了如何自定义HTML报告,但实际实现可能需要根据pytest-html版本调整
    """
    outcome = yield
    report = outcome.get_result()
    
    # 添加自定义信息到报告
    if report.when == "call":
        # 添加环境信息
        report.environment = {
            "Python版本": sys.version,
            "操作系统": sys.platform,
            "测试时间": time.strftime("%Y-%m-%d %H:%M:%S")
        }
        
        # 添加测试元数据
        # 注意:item.fspath和item.lineno在pytest 7.0+中已废弃
        # 使用item.path替代item.fspath
        report.metadata = {
            "测试ID": item.nodeid,
            "测试文件": str(item.path) if hasattr(item, 'path') else str(item.fspath),
            "测试行号": item.obj.__code__.co_firstlineno if hasattr(item, 'obj') else item.lineno
        }

def pytest_html_results_summary(prefix, summary, postfix):
    """
    自定义HTML报告摘要
    """
    prefix.extend([
        "<h2>自定义HTML报告摘要</h2>",
        "<p>这是自定义的HTML报告摘要内容</p>",
        "<p>测试环境:Python 3.8+</p>"
    ])

def pytest_html_results_table_header(cells):
    """
    自定义HTML报告表格头部
    """
    cells.insert(2, "<th>执行时间</th>")
    cells.insert(3, "<th>测试状态</th>")

def pytest_html_results_table_row(report, cells):
    """
    自定义HTML报告表格行
    """
    cells.insert(2, f"<td>{report.duration:.3f}秒</td>")
    
    # 添加测试状态
    status = "通过" if report.passed else "失败"
    cells.insert(3, f"<td>{status}</td>")

6.3 pytest-allure报告生成

allure是一个强大的测试报告框架,提供丰富的报告功能。

注意: allure-pytest插件在不同版本中API可能有所不同,以下示例基于allure-pytest 2.x版本。如果使用allure-pytest 3.x+版本,部分API可能需要调整。

安装allure-pytest
bash 复制代码
# 安装allure命令行工具
# 下载地址:https://github.com/allure-framework/allure2/releases

# 安装allure-pytest插件
pip install allure-pytest

# 或者指定版本
pip install allure-pytest==2.13.2
pytest-allure基础使用
python 复制代码
# test_allure_report.py
import pytest
import allure

# 安装:pip install allure-pytest

@allure.feature("用户管理")
@allure.story("用户登录")
def test_user_login():
    """
    用户登录测试
    """
    # allure.attach的参数顺序:body, name, attachment_type
    allure.attach("用户名: admin", name="用户信息")
    allure.attach("密码: ******", name="密码信息")
    
    assert True

@allure.feature("用户管理")
@allure.story("用户注册")
@allure.severity(allure.severity_level.CRITICAL)
def test_user_registration():
    """
    用户注册测试
    """
    with allure.step("填写注册表单"):
        allure.attach("用户名: testuser", name="用户名")
        allure.attach("邮箱: test@example.com", name="邮箱")
    
    with allure.step("提交注册"):
        assert True

@allure.feature("订单管理")
@allure.story("创建订单")
@allure.title("创建新订单测试")
@allure.description("测试创建新订单的功能")
def test_create_order():
    """
    创建订单测试
    """
    with allure.step("选择商品"):
        allure.attach("商品ID: 12345", name="商品信息")
    
    with allure.step("确认订单"):
        assert True

@allure.feature("订单管理")
@allure.story("取消订单")
@allure.link("https://example.com/issue/123", name="相关Issue")
def test_cancel_order():
    """
    取消订单测试
    """
    with allure.step("查询订单"):
        allure.attach("订单ID: 67890", name="订单信息")
    
    with allure.step("取消订单"):
        assert True

@allure.feature("支付管理")
@allure.story("在线支付")
@allure.issue("https://example.com/issue/456", name="已知问题")
def test_online_payment():
    """
    在线支付测试
    """
    with allure.step("选择支付方式"):
        allure.attach("支付方式: 支付宝", name="支付方式")
    
    with allure.step("完成支付"):
        assert True
pytest-allure高级特性
python 复制代码
# test_allure_advanced.py
import pytest
import allure
import json

@allure.epic("电商平台")
@allure.feature("商品管理")
@allure.story("商品搜索")
class TestProductSearch:
    """
    商品搜索测试类
    """
    
    @allure.title("按关键词搜索商品")
    @allure.description("测试按关键词搜索商品的功能")
    @allure.severity(allure.severity_level.BLOCKER)
    def test_search_by_keyword(self):
        """
        按关键词搜索商品
        """
        with allure.step("输入搜索关键词"):
            search_keyword = "手机"
            allure.attach(search_keyword, name="搜索关键词")
        
        with allure.step("执行搜索"):
            # 模拟搜索结果
            search_results = [
                {"id": 1, "name": "iPhone 13", "price": 5999},
                {"id": 2, "name": "华为 Mate 50", "price": 4999}
            ]
            # 注意:attachment_type参数的使用方式可能因版本而异
            # 在allure-pytest 2.x中,可以使用allure.attachment_type.JSON
            # 在allure-pytest 3.x中,可能需要使用allure.attachment_type.JSON
            allure.attach(
                json.dumps(search_results, ensure_ascii=False),
                name="搜索结果",
                attachment_type=allure.attachment_type.JSON
            )
        
        with allure.step("验证搜索结果"):
            assert len(search_results) > 0
            assert all("手机" in result["name"] for result in search_results)
    
    @allure.title("按价格范围搜索商品")
    @allure.description("测试按价格范围搜索商品的功能")
    @allure.severity(allure.severity_level.NORMAL)
    def test_search_by_price_range(self):
        """
        按价格范围搜索商品
        """
        with allure.step("设置价格范围"):
            min_price = 1000
            max_price = 6000
            allure.attach(f"价格范围: {min_price} - {max_price}", name="价格范围")
        
        with allure.step("执行搜索"):
            # 模拟搜索结果
            search_results = [
                {"id": 1, "name": "iPhone 13", "price": 5999},
                {"id": 3, "name": "小米 12", "price": 2999}
            ]
            allure.attach(
                json.dumps(search_results, ensure_ascii=False),
                name="搜索结果",
                attachment_type=allure.attachment_type.JSON
            )
        
        with allure.step("验证搜索结果"):
            assert all(min_price <= result["price"] <= max_price for result in search_results)

@allure.epic("电商平台")
@allure.feature("购物车管理")
@allure.story("添加商品到购物车")
class TestShoppingCart:
    """
    购物车测试类
    """
    
    @allure.title("添加商品到购物车")
    @allure.description("测试添加商品到购物车的功能")
    @allure.severity(allure.severity_level.CRITICAL)
    def test_add_to_cart(self):
        """
        添加商品到购物车
        """
        with allure.step("选择商品"):
            product = {"id": 1, "name": "iPhone 13", "price": 5999}
            allure.attach(
                json.dumps(product, ensure_ascii=False),
                name="商品信息",
                attachment_type=allure.attachment_type.JSON
            )
        
        with allure.step("添加到购物车"):
            cart_items = [product]
            allure.attach(
                json.dumps(cart_items, ensure_ascii=False),
                name="购物车内容",
                attachment_type=allure.attachment_type.JSON
            )
        
        with allure.step("验证购物车"):
            assert len(cart_items) == 1
            assert cart_items[0]["id"] == 1
    
    @allure.title("从购物车移除商品")
    @allure.description("测试从购物车移除商品的功能")
    @allure.severity(allure.severity_level.NORMAL)
    def test_remove_from_cart(self):
        """
        从购物车移除商品
        """
        with allure.step("初始化购物车"):
            cart_items = [
                {"id": 1, "name": "iPhone 13", "price": 5999},
                {"id": 2, "name": "华为 Mate 50", "price": 4999}
            ]
            allure.attach(
                json.dumps(cart_items, ensure_ascii=False),
                name="初始购物车",
                attachment_type=allure.attachment_type.JSON
            )
        
        with allure.step("移除商品"):
            cart_items = [item for item in cart_items if item["id"] != 1]
            allure.attach(
                json.dumps(cart_items, ensure_ascii=False),
                name="移除后购物车",
                attachment_type=allure.attachment_type.JSON
            )
        
        with allure.step("验证购物车"):
            assert len(cart_items) == 1
            assert cart_items[0]["id"] == 2

6.4 pytest-cov覆盖率报告

pytest-cov插件用于生成代码覆盖率报告。

pytest-cov基础使用
python 复制代码
# test_coverage_basic.py
import pytest

# 安装:pip install pytest-cov

def calculate_sum(a, b):
    """
    计算两个数的和
    """
    return a + b

def calculate_product(a, b):
    """
    计算两个数的积
    """
    return a * b

def calculate_division(a, b):
    """
    计算两个数的商
    """
    if b == 0:
        raise ValueError("除数不能为零")
    return a / b

def test_calculate_sum():
    """
    测试加法计算
    """
    assert calculate_sum(1, 2) == 3
    assert calculate_sum(-1, 1) == 0
    assert calculate_sum(0, 0) == 0

def test_calculate_product():
    """
    测试乘法计算
    """
    assert calculate_product(2, 3) == 6
    assert calculate_product(-1, 1) == -1
    assert calculate_product(0, 5) == 0

def test_calculate_division():
    """
    测试除法计算
    """
    assert calculate_division(6, 2) == 3
    assert calculate_division(10, 5) == 2

def test_calculate_division_by_zero():
    """
    测试除零异常
    """
    with pytest.raises(ValueError):
        calculate_division(1, 0)
pytest-cov配置选项
ini 复制代码
# pytest.ini
[pytest]
# 覆盖率配置
addopts = --cov=src --cov-report=html --cov-report=term-missing --cov-report=xml

# 覆盖率阈值
[coverage:run]
source = src
omit = 
    */tests/*
    */test_*.py
    */__pycache__/*
    */site-packages/*

[coverage:report]
precision = 2
show_missing = True
skip_covered = False
exclude_lines =
    pragma: no cover
    def __repr__
    raise AssertionError
    raise NotImplementedError
    if __name__ == .__main__.:
    if TYPE_CHECKING:
    @abstractmethod
python 复制代码
# conftest.py
import pytest

@pytest.fixture(autouse=True)
def coverage_control():
    """
    覆盖率控制fixture
    """
    import coverage
    cov = coverage.Coverage()
    cov.start()
    
    yield
    
    cov.stop()
    cov.save()
    
    # 打印覆盖率报告
    cov.report()

6.5 覆盖率报告详解

覆盖率报告提供了详细的代码覆盖信息。

覆盖率报告类型
python 复制代码
# test_coverage_types.py
import pytest

class Calculator:
    """
    计算器类
    """
    
    def __init__(self):
        """
        初始化计算器
        """
        self.history = []
    
    def add(self, a, b):
        """
        加法运算
        """
        result = a + b
        self.history.append(f"{a} + {b} = {result}")
        return result
    
    def subtract(self, a, b):
        """
        减法运算
        """
        result = a - b
        self.history.append(f"{a} - {b} = {result}")
        return result
    
    def multiply(self, a, b):
        """
        乘法运算
        """
        result = a * b
        self.history.append(f"{a} * {b} = {result}")
        return result
    
    def divide(self, a, b):
        """
        除法运算
        """
        if b == 0:
            raise ValueError("除数不能为零")
        result = a / b
        self.history.append(f"{a} / {b} = {result}")
        return result
    
    def get_history(self):
        """
        获取计算历史
        """
        return self.history
    
    def clear_history(self):
        """
        清除计算历史
        """
        self.history.clear()

def test_calculator_operations():
    """
    测试计算器操作
    """
    calc = Calculator()
    
    # 测试加法
    assert calc.add(1, 2) == 3
    assert calc.add(-1, 1) == 0
    
    # 测试减法
    assert calc.subtract(5, 3) == 2
    assert calc.subtract(10, 5) == 5
    
    # 测试乘法
    assert calc.multiply(2, 3) == 6
    assert calc.multiply(-1, 1) == -1
    
    # 测试除法
    assert calc.divide(6, 2) == 3
    assert calc.divide(10, 5) == 2
    
    # 测试历史记录
    history = calc.get_history()
    assert len(history) == 6
    
    # 测试清除历史
    calc.clear_history()
    assert len(calc.get_history()) == 0

def test_divide_by_zero():
    """
    测试除零异常
    """
    calc = Calculator()
    
    with pytest.raises(ValueError, match="除数不能为零"):
        calc.divide(1, 0)

6.6 覆盖率阈值设置

可以设置覆盖率阈值,确保代码质量。

覆盖率阈值配置
ini 复制代码
# .coveragerc
[run]
source = src
branch = True

[report]
precision = 2
show_missing = True
skip_covered = False
fail_under = 80

[html]
directory = htmlcov
python 复制代码
# conftest.py
import pytest

@pytest.fixture(autouse=True)
def coverage_threshold_check():
    """
    覆盖率阈值检查fixture
    """
    import coverage
    cov = coverage.Coverage()
    cov.start()
    
    yield
    
    cov.stop()
    cov.save()
    
    # 检查覆盖率阈值
    total = cov.report()
    
    # 获取覆盖率百分比
    cov_data = cov.get_data()
    covered_lines = sum(len(files) for files in cov_data._lines.values())
    total_lines = sum(len(files) for files in cov_data._lines.values())
    
    if total_lines > 0:
        coverage_percent = (covered_lines / total_lines) * 100
        print(f"\n代码覆盖率: {coverage_percent:.2f}%")
        
        if coverage_percent < 80:
            pytest.fail(f"覆盖率 {coverage_percent:.2f}% 低于阈值 80%")

6.7 自定义测试报告

可以创建自定义的测试报告格式。

自定义报告示例
python 复制代码
# conftest.py
import pytest
import json
import time
from datetime import datetime

class CustomReportGenerator:
    """
    自定义报告生成器
    """
    
    def __init__(self):
        self.test_results = []
        self.start_time = None
        self.end_time = None
    
    def start(self):
        """
        开始测试
        """
        self.start_time = time.time()
    
    def finish(self):
        """
        结束测试
        """
        self.end_time = time.time()
    
    def add_result(self, report):
        """
        添加测试结果
        """
        if report.when == "call":
            result = {
                "nodeid": report.nodeid,
                "outcome": report.outcome,
                "duration": report.duration,
                "timestamp": datetime.now().isoformat(),
                "markers": list(report.keywords.keys())
            }
            
            if report.failed:
                result["error"] = str(report.longrepr)
            elif report.skipped:
                result["skip_reason"] = str(report.longrepr)
            
            self.test_results.append(result)
    
    def generate_json_report(self, filename="custom_report.json"):
        """
        生成JSON格式报告
        """
        report_data = {
            "summary": {
                "total": len(self.test_results),
                "passed": sum(1 for r in self.test_results if r["outcome"] == "passed"),
                "failed": sum(1 for r in self.test_results if r["outcome"] == "failed"),
                "skipped": sum(1 for r in self.test_results if r["outcome"] == "skipped"),
                "duration": self.end_time - self.start_time if self.end_time and self.start_time else 0,
                "start_time": datetime.fromtimestamp(self.start_time).isoformat() if self.start_time else None,
                "end_time": datetime.fromtimestamp(self.end_time).isoformat() if self.end_time else None
            },
            "tests": self.test_results
        }
        
        with open(filename, "w", encoding="utf-8") as f:
            json.dump(report_data, f, indent=2, ensure_ascii=False)
        
        print(f"\n自定义JSON报告已生成: {filename}")
    
    def generate_html_report(self, filename="custom_report.html"):
        """
        生成HTML格式报告
        """
        html_content = self._generate_html_content()
        
        with open(filename, "w", encoding="utf-8") as f:
            f.write(html_content)
        
        print(f"\n自定义HTML报告已生成: {filename}")
    
    def _generate_html_content(self):
        """
        生成HTML内容
        """
        total = len(self.test_results)
        passed = sum(1 for r in self.test_results if r["outcome"] == "passed")
        failed = sum(1 for r in self.test_results if r["outcome"] == "failed")
        skipped = sum(1 for r in self.test_results if r["outcome"] == "skipped")
        duration = self.end_time - self.start_time if self.end_time and self.start_time else 0
        
        html = f"""
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>自定义测试报告</title>
    <style>
        body {{ font-family: Arial, sans-serif; margin: 20px; }}
        .summary {{ background: #f0f0f0; padding: 20px; border-radius: 5px; margin-bottom: 20px; }}
        .test-item {{ margin: 10px 0; padding: 10px; border: 1px solid #ddd; border-radius: 3px; }}
        .passed {{ border-left: 4px solid #4CAF50; }}
        .failed {{ border-left: 4px solid #f44336; }}
        .skipped {{ border-left: 4px solid #FF9800; }}
        .duration {{ color: #666; font-size: 0.9em; }}
    </style>
</head>
<body>
    <h1>自定义测试报告</h1>
    <div class="summary">
        <h2>测试摘要</h2>
        <p>总测试数: {total}</p>
        <p>通过: {passed}</p>
        <p>失败: {failed}</p>
        <p>跳过: {skipped}</p>
        <p>总耗时: {duration:.2f}秒</p>
        <p>通过率: {passed/total*100:.1f}%</p>
    </div>
    <h2>测试详情</h2>
"""
        
        for result in self.test_results:
            status_class = result["outcome"]
            html += f"""
    <div class="test-item {status_class}">
        <h3>{result['nodeid']}</h3>
        <p>状态: {result['outcome'].upper()}</p>
        <p class="duration">耗时: {result['duration']:.3f}秒</p>
"""
            if result["outcome"] == "failed" and "error" in result:
                html += f"        <p style='color: red;'>错误: {result['error']}</p>\n"
            elif result["outcome"] == "skipped" and "skip_reason" in result:
                html += f"        <p style='color: orange;'>跳过原因: {result['skip_reason']}</p>\n"
            
            html += "    </div>\n"
        
        html += """
</body>
</html>
"""
        return html

# 创建报告生成器实例
report_generator = CustomReportGenerator()

@pytest.hookimpl(tryfirst=True)
def pytest_configure(config):
    """
    配置钩子
    """
    report_generator.start()

@pytest.hookimpl(trylast=True)
def pytest_runtest_logreport(report):
    """
    记录测试报告
    """
    report_generator.add_result(report)

@pytest.hookimpl(trylast=True)
def pytest_sessionfinish(session, exitstatus):
    """
    会话结束钩子
    """
    report_generator.finish()
    
    # 生成自定义报告
    report_generator.generate_json_report()
    report_generator.generate_html_report()

6.8 JUnit XML报告生成

JUnit XML报告格式广泛用于CI/CD集成。

JUnit XML报告配置
ini 复制代码
# pytest.ini
[pytest]
# JUnit XML报告配置
junit_family = xunit2
junit_suite_name = pytest测试套件
junit_log_passing_tests = true
python 复制代码
# test_junit_xml.py
import pytest

class TestJUnitXML:
    """
    JUnit XML报告测试类
    """
    
    def test_passing_test(self):
        """
        通过的测试
        """
        assert 1 + 1 == 2
    
    def test_failing_test(self):
        """
        失败的测试
        """
        assert 1 + 1 == 3
    
    @pytest.mark.skip(reason="这个测试被跳过")
    def test_skipped_test(self):
        """
        跳过的测试
        """
        assert True
    
    @pytest.mark.xfail(reason="这个测试预期失败")
    def test_xfailed_test(self):
        """
        预期失败的测试
        """
        assert 1 + 1 == 3

6.9 测试报告最佳实践

使用测试报告时需要遵循一些最佳实践。

测试报告最佳实践示例
python 复制代码
# conftest.py
import pytest
import os
from datetime import datetime

# 最佳实践1:使用环境变量控制报告生成
def pytest_addoption(parser):
    """
    添加命令行选项
    """
    parser.addoption("--generate-reports", action="store_true", default=False,
                    help="生成测试报告")
    parser.addoption("--report-dir", action="store", default="reports",
                    help="报告输出目录")

# 最佳实践2:创建报告目录
@pytest.hookimpl(tryfirst=True)
def pytest_configure(config):
    """
    配置钩子
    """
    if config.getoption("--generate-reports"):
        report_dir = config.getoption("--report-dir")
        os.makedirs(report_dir, exist_ok=True)
        print(f"\n报告输出目录: {report_dir}")

# 最佳实践3:添加时间戳到报告文件名
def get_report_filename(base_name, extension):
    """
    获取带时间戳的报告文件名
    """
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    return f"{base_name}_{timestamp}.{extension}"

# 最佳实践4:生成多种格式的报告
@pytest.hookimpl(trylast=True)
def pytest_sessionfinish(session, exitstatus):
    """
    会话结束钩子
    """
    config = session.config
    
    if config.getoption("--generate-reports"):
        report_dir = config.getoption("--report-dir")
        
        # 生成HTML报告
        html_report = os.path.join(report_dir, get_report_filename("report", "html"))
        print(f"\nHTML报告: {html_report}")
        
        # 生成JUnit XML报告
        junit_report = os.path.join(report_dir, get_report_filename("junit", "xml"))
        print(f"JUnit XML报告: {junit_report}")
        
        # 生成覆盖率报告
        cov_report = os.path.join(report_dir, get_report_filename("coverage", "html"))
        print(f"覆盖率报告: {cov_report}")

# 最佳实践5:添加测试环境信息到报告
@pytest.hookimpl(tryfirst=True)
def pytest_report_header(config, startdir):
    """
    报告头部信息
    """
    lines = []
    lines.append("=== 测试环境信息 ===")
    lines.append(f"测试目录: {startdir}")
    lines.append(f"Python版本: {config.pythonversion}")
    lines.append(f"pytest版本: {pytest.__version__}")
    lines.append(f"操作系统: {os.name}")
    lines.append(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    return "\n".join(lines)

# 最佳实践6:自定义测试状态显示
@pytest.hookimpl(trylast=True)
def pytest_report_teststatus(report, config):
    """
    自定义测试状态显示
    """
    if report.when == "call":
        if report.passed:
            return "✓", "PASSED", {"green": True}
        elif report.failed:
            return "✗", "FAILED", {"red": True}
        elif report.skipped:
            return "⊘", "SKIPPED", {"yellow": True}
    return None

# 最佳实践7:添加测试摘要信息
@pytest.hookimpl(trylast=True)
def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """
    终端摘要报告
    """
    stats = terminalreporter.stats
    total = sum(len(stats.get(key, [])) for key in ["passed", "failed", "skipped"])
    passed = len(stats.get("passed", []))
    failed = len(stats.get("failed", []))
    skipped = len(stats.get("skipped", []))
    
    print("\n=== 测试摘要 ===")
    print(f"总测试数: {total}")
    print(f"通过: {passed}")
    print(f"失败: {failed}")
    print(f"跳过: {skipped}")
    if total > 0:
        print(f"通过率: {passed/total*100:.1f}%")

# 最佳实践8:记录测试执行时间
@pytest.hookimpl(trylast=True)
def pytest_runtest_logreport(report):
    """
    记录测试执行时间
    """
    if report.when == "call" and report.duration > 1.0:
        print(f"\n警告: 测试 {report.nodeid} 执行时间过长 ({report.duration:.2f}秒)")

# 最佳实践9:添加失败测试的详细信息
@pytest.hookimpl(trylast=True)
def pytest_runtest_makereport(item, call):
    """
    创建测试报告
    """
    if call.when == "call" and call.excinfo:
        # 添加失败测试的详细信息
        print(f"\n失败测试: {item.nodeid}")
        print(f"失败原因: {call.excinfo.value}")
        print(f"失败位置: {call.excinfo.tb}")

# 最佳实践10:生成测试趋势报告
class TestTrendTracker:
    """
    测试趋势跟踪器
    """
    
    def __init__(self):
        self.history = []
    
    def add_record(self, total, passed, failed, skipped, duration):
        """
        添加测试记录
        """
        record = {
            "timestamp": datetime.now().isoformat(),
            "total": total,
            "passed": passed,
            "failed": failed,
            "skipped": skipped,
            "duration": duration
        }
        self.history.append(record)
    
    def generate_trend_report(self, filename="test_trend.json"):
        """
        生成趋势报告
        """
        import json
        
        with open(filename, "w", encoding="utf-8") as f:
            json.dump(self.history, f, indent=2, ensure_ascii=False)
        
        print(f"\n测试趋势报告已生成: {filename}")

trend_tracker = TestTrendTracker()

@pytest.hookimpl(trylast=True)
def pytest_sessionfinish_with_trend(session, exitstatus):
    """
    会话结束钩子 - 趋势跟踪
    """
    stats = session.config.pluginmanager.get_plugin("terminalreporter").stats
    total = sum(len(stats.get(key, [])) for key in ["passed", "failed", "skipped"])
    passed = len(stats.get("passed", []))
    failed = len(stats.get("failed", []))
    skipped = len(stats.get("skipped", []))
    
    # 计算总耗时
    duration = sum(report.duration for report in stats.get("passed", []) + 
                  stats.get("failed", []) + stats.get("skipped", []))
    
    # 添加记录
    trend_tracker.add_record(total, passed, failed, skipped, duration)
    
    # 生成趋势报告
    trend_tracker.generate_trend_report()

6.10 报告集成与CI/CD

测试报告需要与CI/CD系统集成。

CI/CD集成示例
yaml 复制代码
# .github/workflows/pytest.yml
name: Pytest Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        python-version: [3.8, 3.9, 3.10]
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pytest pytest-cov pytest-html pytest-xdist
        pip install -r requirements.txt
    
    - name: Run tests with coverage
      run: |
        pytest --cov=src --cov-report=xml --cov-report=html --html=report.html --junitxml=junit.xml
    
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v2
      with:
        file: ./coverage.xml
        flags: unittests
        name: codecov-umbrella
    
    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v2
      with:
        name: test-results-py${{ matrix.python-version }}
        path: |
          report.html
          junit.xml
          htmlcov/
python 复制代码
# conftest.py
import pytest
import os

# CI/CD环境检测
def is_ci_environment():
    """
    检测是否在CI/CD环境中运行
    """
    return os.getenv("CI") == "true" or os.getenv("GITHUB_ACTIONS") == "true"

@pytest.hookimpl(tryfirst=True)
def pytest_configure_ci(config):
    """
    CI/CD环境配置
    """
    if is_ci_environment():
        print("\n检测到CI/CD环境")
        
        # CI/CD环境下的特殊配置
        config.addinivalue_line("markers", "ci: CI/CD专用测试")
        
        # 设置更严格的覆盖率要求
        os.environ["COVERAGE_THRESHOLD"] = "90"

@pytest.hookimpl(trylast=True)
def pytest_sessionfinish_ci(session, exitstatus):
    """
    CI/CD环境会话结束
    """
    if is_ci_environment():
        print("\nCI/CD测试完成")
        
        # 生成CI/CD专用报告
        print(f"退出状态码: {exitstatus}")
        
        # 检查测试结果
        if exitstatus != 0:
            print("测试失败,CI/CD流程将终止")
        else:
            print("测试通过,CI/CD流程继续")

7. pytest插件生态与高级特性

7.1 pytest插件生态概述

pytest拥有丰富的插件生态,提供了各种扩展功能。

pytest插件分类

pytest插件生态
测试增强插件
报告生成插件
并发执行插件
框架集成插件
性能测试插件
自定义插件
pytest-mock
pytest-cov
pytest-timeout
pytest-html
allure-pytest
pytest-reportlog
pytest-xdist
pytest-parallel
pytest-forked
pytest-django
pytest-flask
pytest-asyncio
pytest-benchmark
pytest-profiling
pytest-speed
自定义钩子
自定义标记
自定义断言

插件管理基础
python 复制代码
# test_plugin_management.py
import pytest

# 查看已安装的插件
# 命令:pytest --version
# 输出示例:
# pytest 7.4.0
# plugins: xdist-3.3.1, cov-4.1.0, html-3.2.0

def test_plugin_info(request):
    """
    测试插件信息
    注意:pytest.config在pytest 7.0+中已被移除,需要使用request.config
    """
    # 获取pytest配置
    config = request.config
    
    # 获取插件管理器
    plugin_manager = config.pluginmanager
    
    # 获取已安装的插件
    plugins = plugin_manager.get_plugins()
    
    print(f"\n已安装插件数量: {len(plugins)}")
    for plugin in plugins:
        print(f"插件名称: {plugin.__name__ if hasattr(plugin, '__name__') else plugin}")
    
    assert len(plugins) > 0

7.2 常用pytest插件介绍

pytest生态系统中有许多常用插件,提供了丰富的功能。

pytest-xdist并发测试
python 复制代码
# test_xdist_plugin.py
import pytest
import time

# 安装:pip install pytest-xdist

def test_xdist_basic_1():
    """
    并发测试1
    """
    time.sleep(0.5)
    assert True

def test_xdist_basic_2():
    """
    并发测试2
    """
    time.sleep(0.5)
    assert True

def test_xdist_basic_3():
    """
    并发测试3
    """
    time.sleep(0.5)
    assert True

def test_xdist_basic_4():
    """
    并发测试4
    """
    time.sleep(0.5)
    assert True

# 命令:pytest -n 4 test_xdist_plugin.py
# -n 或 --numprocesses:指定并发进程数
# -n auto:自动检测CPU核心数

# pytest-xdist配置
"""
在pytest.ini中配置:

[pytest]
addopts = -n auto
"""
pytest-asyncio异步测试
python 复制代码
# test_asyncio_plugin.py
import pytest
import asyncio

# 安装:pip install pytest-asyncio

@pytest.mark.asyncio
async def test_async_basic():
    """
    基础异步测试
    """
    await asyncio.sleep(0.1)
    assert True

@pytest.mark.asyncio
async def test_async_with_fixture(asyncio_loop):
    """
    带fixture的异步测试
    """
    await asyncio.sleep(0.1)
    assert asyncio_loop is not None

@pytest.fixture
async def async_resource():
    """
    异步fixture
    """
    await asyncio.sleep(0.1)
    return {"data": "async_data"}

@pytest.mark.asyncio
async def test_async_with_resource(async_resource):
    """
    使用异步资源
    """
    assert async_resource["data"] == "async_data"

# pytest-asyncio配置
"""
在pytest.ini中配置:

[pytest]
asyncio_mode = auto
"""

# 异步模式说明
"""
asyncio_mode参数:
- auto:自动检测async/await语法
- strict:严格模式,需要显式标记
- auto_strict:自动检测但严格模式
"""
pytest-timeout超时控制
python 复制代码
# test_timeout_plugin.py
import pytest
import time

# 安装:pip install pytest-timeout

def test_timeout_fast():
    """
    快速测试
    """
    time.sleep(0.1)
    assert True

def test_timeout_slow():
    """
    慢速测试
    """
    time.sleep(0.5)
    assert True

@pytest.mark.timeout(1)
def test_timeout_with_marker():
    """
    带超时标记的测试
    """
    time.sleep(0.5)
    assert True

# pytest-timeout配置
"""
在pytest.ini中配置:

[pytest]
timeout = 5  # 全局超时时间(秒)
timeout_method = thread  # 超时方法:thread或signal
"""

# 超时方法说明
"""
timeout_method参数:
- thread:使用线程实现超时(跨平台)
- signal:使用信号实现超时(仅Unix)
"""

7.3 pytest-xdist并发测试详解

pytest-xdist提供了强大的并发测试功能。

pytest-xdist高级特性
python 复制代码
# test_xdist_advanced.py
import pytest

# 并发测试分组
@pytest.mark.xdist_group("group1")
def test_xdist_group_1_1():
    """
    并发测试组1-1
    """
    assert True

@pytest.mark.xdist_group("group1")
def test_xdist_group_1_2():
    """
    并发测试组1-2
    """
    assert True

@pytest.mark.xdist_group("group2")
def test_xdist_group_2_1():
    """
    并发测试组2-1
    """
    assert True

@pytest.mark.xdist_group("group2")
def test_xdist_group_2_2():
    """
    并发测试组2-2
    """
    assert True

# 命令:pytest -n 4 test_xdist_advanced.py
# 同一组的测试会在同一个worker进程中执行

# 锁定测试顺序
"""
命令:pytest -n 4 --dist=loadscope test_xdist_advanced.py

--dist参数:
- each:每个测试独立分发
- loadscope:按模块/类分发,保持测试顺序
- loadfile:按文件分发
"""

# worker间通信
@pytest.fixture(scope="session")
def shared_data():
    """
    共享数据fixture
    在xdist中,session作用域的fixture在每个worker中独立创建
    """
    return {"counter": 0}

def test_shared_data_1(shared_data):
    """
    测试共享数据1
    """
    shared_data["counter"] += 1
    assert shared_data["counter"] == 1

def test_shared_data_2(shared_data):
    """
    测试共享数据2
    """
    shared_data["counter"] += 1
    assert shared_data["counter"] == 1  # 每个worker独立计数

7.4 pytest-asyncio异步测试详解

pytest-asyncio提供了完整的异步测试支持。

pytest-asyncio高级特性
python 复制代码
# test_asyncio_advanced.py
import pytest
import asyncio

# 异步事件循环配置
@pytest.fixture
def event_loop():
    """
    自定义事件循环fixture
    """
    loop = asyncio.new_event_loop()
    yield loop
    loop.close()

# 异步fixture
@pytest.fixture
async def async_database():
    """
    异步数据库fixture
    """
    # 模拟异步数据库连接
    await asyncio.sleep(0.1)
    db = {"connection": "active", "data": []}
    
    yield db
    
    # 清理
    await asyncio.sleep(0.1)
    db["connection"] = "closed"

@pytest.mark.asyncio
async def test_async_database(async_database):
    """
    测试异步数据库
    """
    assert async_database["connection"] == "active"
    async_database["data"].append("test_data")
    assert len(async_database["data"]) == 1

# 异步上下文管理器
class AsyncContextManager:
    """
    异步上下文管理器
    """
    
    async def __aenter__(self):
        await asyncio.sleep(0.1)
        return {"resource": "active"}
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await asyncio.sleep(0.1)
        return False

@pytest.mark.asyncio
async def test_async_context_manager():
    """
    测试异步上下文管理器
    """
    async with AsyncContextManager() as resource:
        assert resource["resource"] == "active"

# 异步生成器
async def async_generator():
    """
    异步生成器
    """
    for i in range(3):
        await asyncio.sleep(0.1)
        yield i

@pytest.mark.asyncio
async def test_async_generator():
    """
    测试异步生成器
    """
    results = []
    async for value in async_generator():
        results.append(value)
    
    assert results == [0, 1, 2]

# 并发异步测试
@pytest.mark.asyncio
async def test_async_concurrent():
    """
    测试并发异步操作
    """
    async def async_task(name, delay):
        await asyncio.sleep(delay)
        return name
    
    tasks = [
        async_task("task1", 0.1),
        async_task("task2", 0.2),
        async_task("task3", 0.3)
    ]
    
    results = await asyncio.gather(*tasks)
    assert len(results) == 3
    assert "task1" in results

7.5 pytest-django Django测试

pytest-django提供了Django框架的完整测试支持。

pytest-django基础使用
python 复制代码
# test_django_plugin.py
import pytest
from django.test import TestCase
from django.contrib.auth.models import User

# 安装:pip install pytest-django

# pytest-django配置
"""
在pytest.ini中配置:

[pytest]
DJANGO_SETTINGS_MODULE = myproject.settings
python_files = test_*.py
python_classes = Test*
python_functions = test_*

# 或者在conftest.py中配置:

import pytest

@pytest.fixture(scope='session')
def django_db_setup():
    """
    Django数据库设置
    """
    pass
"""

@pytest.mark.django_db
def test_create_user():
    """
    测试创建用户
    """
    user = User.objects.create_user(
        username='testuser',
        email='test@example.com',
        password='testpass123'
    )
    assert user.username == 'testuser'
    assert user.email == 'test@example.com'

@pytest.mark.django_db
def test_user_authentication():
    """
    测试用户认证
    """
    user = User.objects.create_user(
        username='testuser',
        password='testpass123'
    )
    
    authenticated = user.check_password('testpass123')
    assert authenticated

# Django测试客户端
@pytest.mark.django_db
def test_client_request(client):
    """
    测试客户端请求
    """
    response = client.get('/')
    assert response.status_code == 200

# Django URL测试
from django.urls import reverse

@pytest.mark.django_db
def test_url_resolution():
    """
    测试URL解析
    """
    url = reverse('admin:index')
    assert url == '/admin/'

# Django模型测试
@pytest.mark.django_db
class TestUserModel(TestCase):
    """
    用户模型测试类
    """
    
    def setUp(self):
        """
        测试设置
        """
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
    
    def test_user_creation(self):
        """
        测试用户创建
        """
        assert User.objects.count() == 1
        assert self.user.username == 'testuser'
    
    def test_user_str(self):
        """
        测试用户字符串表示
        """
        assert str(self.user) == 'testuser'

7.6 pytest-flask Flask测试

pytest-flask提供了Flask框架的完整测试支持。

pytest-flask基础使用
python 复制代码
# test_flask_plugin.py
import pytest
from flask import Flask, jsonify

# 安装:pip install pytest-flask

# pytest-flask配置
"""
在conftest.py中配置:

import pytest
from myapp import create_app

@pytest.fixture
def app():
    """
    应用fixture
    """
    app = create_app('testing')
    return app

@pytest.fixture
def client(app):
    """
    客户端fixture
    """
    return app.test_client()

@pytest.fixture
def runner(app):
    """
    测试运行器fixture
    """
    return app.test_cli_runner()
"""

def test_index(client):
    """
    测试首页
    """
    response = client.get('/')
    assert response.status_code == 200
    assert b'Hello' in response.data

def test_api_endpoint(client):
    """
    测试API端点
    """
    response = client.get('/api/data')
    assert response.status_code == 200
    data = response.get_json()
    assert 'data' in data

def test_post_request(client):
    """
    测试POST请求
    """
    response = client.post('/api/users', json={
        'name': 'Test User',
        'email': 'test@example.com'
    })
    assert response.status_code == 201
    data = response.get_json()
    assert data['name'] == 'Test User'

def test_authentication(client, auth):
    """
    测试认证
    """
    # 登录
    response = auth.login()
    assert response.status_code == 200
    
    # 访问受保护的资源
    response = client.get('/protected')
    assert response.status_code == 200

# Flask认证fixture
@pytest.fixture
def auth(client):
    """
    认证fixture
    """
    class AuthActions:
        def __init__(self, client):
            self._client = client
        
        def login(self, username='test', password='test'):
            return self._client.post('/auth/login', data={
                'username': username,
                'password': password
            })
        
        def logout(self):
            return self._client.get('/auth/logout')
    
    return AuthActions(client)

7.7 pytest-benchmark性能测试

pytest-benchmark提供了性能测试和基准测试功能。

pytest-benchmark基础使用
python 复制代码
# test_benchmark_plugin.py
import pytest

# 安装:pip install pytest-benchmark

def test_list_append(benchmark):
    """
    测试列表追加性能
    """
    def append_to_list():
        lst = []
        for i in range(1000):
            lst.append(i)
        return lst
    
    result = benchmark(append_to_list)
    assert len(result) == 1000

def test_dict_lookup(benchmark):
    """
    测试字典查找性能
    """
    data = {i: i * 2 for i in range(1000)}
    
    def lookup_dict():
        return data[500]
    
    result = benchmark(lookup_dict)
    assert result == 1000

def test_string_concatenation(benchmark):
    """
    测试字符串连接性能
    """
    def concat_strings():
        return ''.join(str(i) for i in range(1000))
    
    result = benchmark(concat_strings)
    assert len(result) == 2890

# 基准测试分组
class TestBenchmarkGroup:
    """
    基准测试组
    """
    
    def test_list_creation(self, benchmark):
        """
        测试列表创建
        """
        result = benchmark(list, range(1000))
        assert len(result) == 1000
    
    def test_set_creation(self, benchmark):
        """
        测试集合创建
        """
        result = benchmark(set, range(1000))
        assert len(result) == 1000

# 基准测试配置
"""
命令行参数:
--benchmark-only:只运行基准测试
--benchmark-autosave:自动保存基准测试结果
--benchmark-save:保存基准测试结果到文件
--benchmark-compare:与之前的结果比较
--benchmark-columns:指定显示的列
"""

# 基准测试装饰器
@pytest.mark.benchmark(group="string_operations")
def test_string_operations(benchmark):
    """
    字符串操作基准测试
    """
    def string_operations():
        s = "hello world"
        s = s.upper()
        s = s.lower()
        s = s.replace(" ", "_")
        return s
    
    result = benchmark(string_operations)
    assert result == "hello_world"

7.8 自定义pytest插件开发

可以开发自定义的pytest插件来扩展功能。

自定义插件开发示例
python 复制代码
# my_pytest_plugin.py
import pytest

def pytest_configure(config):
    """
    配置钩子
    """
    config.addinivalue_line("markers", "custom: 自定义标记")
    print("\n[自定义插件] pytest_configure: 插件已加载")

def pytest_collection_modifyitems(session, config, items):
    """
    修改测试项钩子
    """
    for item in items:
        # 为所有测试添加自定义标记
        if "custom" not in item.keywords:
            item.add_marker(pytest.mark.custom)

def pytest_runtest_setup(item):
    """
    测试设置钩子
    """
    print(f"\n[自定义插件] pytest_runtest_setup: {item.name}")

def pytest_runtest_teardown(item, nextitem):
    """
    测试清理钩子
    """
    print(f"[自定义插件] pytest_runtest_teardown: {item.name}")

def pytest_report_header(config, startdir):
    """
    报告头部钩子
    """
    return "\n[自定义插件] pytest_report_header: 自定义测试报告"

def pytest_terminal_summary(terminalreporter, exitstatus, config):
    """
    终端摘要钩子
    """
    print("\n[自定义插件] pytest_terminal_summary: 测试完成")

# 插件入口点
"""
在setup.py中配置:

from setuptools import setup

setup(
    name='my-pytest-plugin',
    version='0.1.0',
    packages=['my_pytest_plugin'],
    entry_points={
        'pytest11': [
            'my_plugin = my_pytest_plugin',
        ],
    },
)
"""
插件注册与安装
python 复制代码
# setup.py
from setuptools import setup

setup(
    name='my-pytest-plugin',
    version='0.1.0',
    description='My custom pytest plugin',
    packages=['my_pytest_plugin'],
    install_requires=[
        'pytest>=7.0.0',
    ],
    entry_points={
        'pytest11': [
            'my_plugin = my_pytest_plugin',
        ],
    },
    classifiers=[
        'Framework :: Pytest',
        'Programming Language :: Python :: 3',
    ],
)

# 安装插件
# pip install -e .

# 验证插件安装
# pytest --version
# 输出示例:
# pytest 7.4.0
# plugins: my-pytest-plugin-0.1.0, ...

7.9 插件管理与配置

有效的插件管理对于维护测试环境至关重要。

插件管理最佳实践
python 复制代码
# conftest.py
import pytest

# 插件配置管理
def pytest_configure(config):
    """
    插件配置管理
    """
    # 检查必需插件
    required_plugins = ['xdist', 'cov', 'html']
    plugin_manager = config.pluginmanager
    
    for plugin_name in required_plugins:
        plugin_found = any(
            plugin_name in str(plugin) 
            for plugin in plugin_manager.get_plugins()
        )
        
        if not plugin_found:
            print(f"\n警告: 必需插件 '{plugin_name}' 未安装")

# 插件版本控制
def pytest_report_header(config, startdir):
    """
    报告插件版本信息
    """
    lines = ["=== 插件版本信息 ==="]
    
    # 获取插件信息
    plugin_manager = config.pluginmanager
    plugins = plugin_manager.get_plugins()
    
    for plugin in plugins:
        if hasattr(plugin, '__version__'):
            plugin_name = plugin.__name__
            plugin_version = plugin.__version__
            lines.append(f"{plugin_name}: {plugin_version}")
    
    return "\n".join(lines)

# 插件依赖管理
"""
在requirements.txt中管理插件依赖:

pytest>=7.4.0
pytest-xdist>=3.3.0
pytest-cov>=4.1.0
pytest-html>=3.2.0
pytest-asyncio>=0.21.0
pytest-timeout>=2.1.0
pytest-benchmark>=4.0.0
"""

# 插件配置文件
"""
在pytest.ini中配置插件:

[pytest]
# 插件配置
addopts = 
    --strict-markers
    --strict-config
    -v
    --tb=short

# 禁用特定插件
# addopts = -p no:xdist

# 启用特定插件
# addopts = -p my_plugin
"""

7.10 插件最佳实践

使用pytest插件时需要遵循一些最佳实践。

插件使用最佳实践
python 复制代码
# conftest.py
import pytest
import os

# 最佳实践1:按需启用插件
def pytest_addoption(parser):
    """
    添加插件控制选项
    """
    parser.addoption("--enable-xdist", action="store_true", default=False,
                    help="启用xdist并发测试")
    parser.addoption("--enable-cov", action="store_true", default=False,
                    help="启用覆盖率测试")

# 最佳实践2:条件加载插件
@pytest.hookimpl(tryfirst=True)
def pytest_configure_conditional(config):
    """
    条件配置插件
    """
    # 根据环境变量启用插件
    if os.getenv("CI"):
        print("CI环境:启用所有插件")
        config.addinivalue_line("markers", "ci: CI专用测试")
    else:
        print("本地环境:启用基础插件")

# 最佳实践3:插件冲突解决
def pytest_configure_conflict_resolution(config):
    """
    解决插件冲突
    """
    # 检查插件冲突
    xdist_enabled = config.getoption("--enable-xdist")
    cov_enabled = config.getoption("--enable-cov")
    
    if xdist_enabled and cov_enabled:
        print("\n警告: xdist和cov可能存在冲突")
        print("建议: 使用 --dist=loadscope 参数")

# 最佳实践4:插件性能优化
@pytest.hookimpl(tryfirst=True)
def pytest_configure_performance(config):
    """
    插件性能优化
    """
    # 在本地开发时禁用耗时插件
    if not os.getenv("CI"):
        print("本地开发:禁用耗时插件")
        # 可以在这里禁用某些插件

# 最佳实践5:插件错误处理
@pytest.hookimpl(tryfirst=True)
def pytest_configure_error_handling(config):
    """
    插件错误处理
    """
    try:
        # 尝试配置插件
        config.addinivalue_line("markers", "custom: 自定义标记")
    except Exception as e:
        print(f"\n插件配置错误: {e}")
        # 不抛出异常,避免影响pytest执行

# 最佳实践6:插件文档化
"""
在conftest.py中添加插件使用文档:

# 插件使用说明
# 1. pytest-xdist: 并发测试
#    命令: pytest -n 4
#    说明: 使用4个进程并发执行测试
#
# 2. pytest-cov: 覆盖率测试
#    命令: pytest --cov=src
#    说明: 生成代码覆盖率报告
#
# 3. pytest-html: HTML报告
#    命令: pytest --html=report.html
#    说明: 生成HTML格式的测试报告
"""

# 最佳实践7:插件版本兼容性
def pytest_configure_version_compatibility(config):
    """
    插件版本兼容性检查
    """
    # 检查pytest版本
    pytest_version = config.pluginmanager.get_plugin("pytest").__version__
    required_version = "7.0.0"
    
    if pytest_version < required_version:
        print(f"\n警告: pytest版本 {pytest_version} 低于推荐版本 {required_version}")

# 最佳实践8:插件配置验证
def pytest_configure_validation(config):
    """
    插件配置验证
    """
    # 验证必需的配置
    required_config = [
        ("DJANGO_SETTINGS_MODULE", "Django设置模块"),
        ("TEST_DATABASE_URL", "测试数据库URL")
    ]
    
    for env_var, description in required_config:
        if not os.getenv(env_var):
            print(f"\n警告: 缺少环境变量 {env_var} ({description})")

# 最佳实践9:插件监控
class PluginMonitor:
    """
    插件监控器
    """
    
    def __init__(self):
        self.plugin_calls = {}
    
    def record_call(self, plugin_name, hook_name):
        """
        记录插件调用
        """
        key = f"{plugin_name}.{hook_name}"
        self.plugin_calls[key] = self.plugin_calls.get(key, 0) + 1
    
    def report(self):
        """
        报告插件使用情况
        """
        print("\n=== 插件使用情况 ===")
        for call, count in sorted(self.plugin_calls.items()):
            print(f"{call}: {count} 次")

plugin_monitor = PluginMonitor()

@pytest.hookimpl(tryfirst=True)
def pytest_runtest_setup_with_monitor(item):
    """
    带监控的测试设置
    """
    plugin_monitor.record_call("pytest", "runtest_setup")

# 最佳实践10:插件卸载与清理
def pytest_unconfigure_cleanup(config):
    """
    插件卸载与清理
    """
    print("\n[插件清理] 开始清理插件资源")
    
    # 清理插件创建的资源
    # 关闭数据库连接
    # 清理临时文件
    # 释放内存
    
    print("[插件清理] 插件资源清理完成")
相关推荐
代码探秘者2 小时前
【算法篇】1.双指针
java·数据结构·人工智能·后端·python·算法
Mr_Xuhhh2 小时前
测试相关面试题
集成测试
Rolei_zl2 小时前
AIGC(生成式AI)试用 48 -- AI与软件开发过程3
python·aigc
qq_416018722 小时前
持续集成/持续部署(CI/CD) for Python
jvm·数据库·python
qq_416018722 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python
测试19982 小时前
功能测试、自动化测试、性能测试的区别?
自动化测试·软件测试·python·功能测试·测试工具·性能测试·安全性测试
MyY_DO2 小时前
序列模型说人话
python
AC赳赳老秦2 小时前
使用OpenClaw tavily-search技能高效撰写工作报告:以人工智能在医疗行业的应用为例
运维·人工智能·python·flask·自动化·deepseek·openclaw
姚青&2 小时前
Pytest 测试用例并行运行与分布式运行
分布式·测试用例·pytest