pytest相关

6 minute read

Pytest 测试框架 系统学习大纲

一、Pytest 概述

  • 1.1 Pytest 简介
    • 什么是 Pytest
    • Pytest 的特点和优势
    • 与其他测试框架对比(unittest, nose)
    详情关于pytest和unittest对比
    对比维度 pytest unittest
    设计哲学 更灵活、简洁,遵循“约定优于配置”原则。 基于 Java 的 JUnit 框架,采用经典的 xUnit 风格,结构严谨。
    编写风格 使用普通的 Python 函数和 assert 语句,代码更简洁。 必须继承 unittest.TestCase 类,并使用 self.assertXxx() 系列方法。
    断言方式 直接使用 Python 原生的 assert 语句,失败时信息更清晰易读。 使用 self.assertXxx() 方法(如 assertEqual, assertTrue)。
    夹具管理 通过 @pytest.fixture 装饰器提供强大、灵活的夹具系统,支持作用域和自动使用。 通过 setUp/tearDown(方法级)和 setUpClass/tearDownClass(类级)管理。
    测试发现 自动发现以 test_ 开头的函数和以 Test 开头的类中的 test_ 方法。 自动发现继承 unittest.TestCase 的类中的 test_ 开头的方法。
    插件生态 拥有极其丰富和强大的插件生态系统(如并发执行、HTML报告、覆盖率集成等)。 生态相对简单,功能扩展主要依赖原生模块或少量第三方库。
    执行控制 命令行功能强大,支持通过 -k 筛选用例、-m 标记用例、--lf 只运行上次失败的用例等。 命令行功能相对基础。
    报告输出 默认输出简洁,失败时提供非常详细的上下文差异分析。 输出为标准 xUnit 风格报告。
    与unittest兼容 可以无缝运行 unittest 测试用例,无需修改代码。 无法直接运行 pytest 风格的测试函数。
  • 1.2 安装与配置
    • pip 安装

    • 虚拟环境配置
    • 验证安装
    pip install pytest
    pytest --version
    
  • 1.3 第一个测试用例
    • 简单测试函数
    • 运行测试
    • 测试结果解读
    # test_sample.py
    def func(x):
        return x + 1
    
    def test_answer():
        assert func(3) == 5
    

二、测试发现与执行

  • 2.1 测试发现规则
    • 文件命名规则:test_*.py 或 *_test.py
    • 函数命名规则:test_*
    • 类命名规则:Test*
    • 方法命名规则:test_*
  • 2.2 执行测试的方式
    • 运行所有测试
    • 运行指定模块
    • 运行指定目录
    • 运行指定测试函数
    # 运行所有测试
    pytest
    
    # 运行指定文件
    pytest test_module.py
    
    # 运行指定测试函数
    pytest test_module.py::test_function
    
    # 运行指定类
    pytest test_module.py::TestClass
    
    # 运行指定类中的方法
    pytest test_module.py::TestClass::test_method
    
  • 2.3 常用命令行选项
    • -v: 详细输出
    • -q: 简洁输出
    • -s: 显示 print 输出
    • -k: 关键字过滤
    • -m: 标记过滤
    • -x: 遇到失败立即停止
    • –maxfail=n: 指定最大失败数
    • –collect-only: 收集但不执行
    • –tb=style: 设置回溯显示样式
    pytest -v -s
    pytest -k "login"
    pytest -m "slow"
    pytest --maxfail=2
    

三、测试用例编写规范

  • 3.1 测试函数结构
    • Arrange-Act-Assert 模式
    • 函数签名
    • 返回值
  • 3.2 断言使用
    • assert 语句
    • 内置断言
    • 断言重写机制
    def test_addition():
        # Arrange
        a, b = 1, 2
            
        # Act
        result = a + b
            
        # Assert
        assert result == 3
            
    def test_list_operations():
        items = [1, 2, 3]
        assert len(items) == 3
        assert 2 in items
        assert items[0] == 1
    
  • 3.3 测试异常
    • pytest.raises
    • 异常匹配
    • 异常消息验证
    import pytest
    
    def test_divide_by_zero():
        with pytest.raises(ZeroDivisionError):
            1 / 0
    
    def test_divide_by_zero_with_message():
        with pytest.raises(ZeroDivisionError, match="division by zero"):
            1 / 0
    
  • 3.4 测试类组织
    • 测试类结构
    • 类级别设置
    • 实例方法
    class TestCalculator:
        def test_addition(self):
            assert 1 + 1 == 2
            
        def test_subtraction(self):
            assert 5 - 3 == 2
    

四、固件(Fixture)

  • 4.1 什么是 Fixture
    • 定义和用途
    • 生命周期
    • 作用域
  • 4.2 基本用法
    • @pytest.fixture 装饰器
    • Fixture 参数
    • 使用 Fixture
    import pytest
    
    @pytest.fixture
    def sample_data():
        return {"name": "Alice", "age": 30}
    
    def test_user_data(sample_data):
        assert sample_data["name"] == "Alice"
        assert sample_data["age"] == 30
    
  • 4.3 Fixture 作用域
    • function: 函数级别
    • class: 类级别
    • module: 模块级别
    • session: 会话级别
    @pytest.fixture(scope="session")
    def db_connection():
        # 建立数据库连接
        conn = create_connection()
        yield conn
        # 清理
        conn.close()
    
    @pytest.fixture(scope="module")
    def setup_data():
        return {"key": "value"}
    
  • 4.4 Fixture 自动使用
    • autouse=True
    • 全局 Fixture
    @pytest.fixture(autouse=True)
    def setup_each_test():
        print("\n=== 开始测试 ===")
        yield
        print("\n=== 结束测试 ===")
    
  • 4.5 Fixture 之间的依赖
    • Fixture 调用其他 Fixture
    • 依赖关系
    @pytest.fixture
    def user():
        return {"name": "Alice"}
    
    @pytest.fixture
    def user_with_age(user):
        user["age"] = 30
        return user
    
    def test_user_info(user_with_age):
        assert user_with_age["name"] == "Alice"
        assert user_with_age["age"] == 30
    
  • 4.6 内置 Fixture
    • tmp_path: 临时目录
    • capsys: 捕获输出
    • monkeypatch: 猴子补丁
    • mocker: pytest-mock
    def test_tmp_path(tmp_path):
        file_path = tmp_path / "test.txt"
        file_path.write_text("Hello")
        assert file_path.read_text() == "Hello"
    
    def test_output(capsys):
        print("Hello World")
        captured = capsys.readouterr()
        assert captured.out == "Hello World\n"
    

五、参数化测试

  • 5.1 @pytest.mark.parametrize
    • 基本语法
    • 多参数
    • 参数组合
    import pytest
    
    @pytest.mark.parametrize("input,expected", [
        (1, 2),
        (2, 3),
        (3, 4)
    ])
    def test_increment(input, expected):
        assert input + 1 == expected
    
  • 5.2 参数化 Fixture
    • indirect 参数
    • 动态参数
    @pytest.fixture
    def data(request):
        return request.param * 2
    
    @pytest.mark.parametrize("data", [1, 2, 3], indirect=True)
    def test_data(data):
        assert data % 2 == 0
    
  • 5.3 参数化策略
    • 笛卡尔积
    • 自定义参数生成
    • 从文件读取参数

六、标记(Mark)

  • 6.1 内置标记
    • @pytest.mark.skip: 跳过测试
    • @pytest.mark.skipif: 条件跳过
    • @pytest.mark.xfail: 预期失败
    • @pytest.mark.parametrize: 参数化
    import pytest
    import sys
    
    @pytest.mark.skip(reason="功能未实现")
    def test_unimplemented():
        assert False
    
    @pytest.mark.skipif(sys.version_info < (3, 8), reason="需要 Python 3.8+")
    def test_python38_feature():
        assert True
    
    @pytest.mark.xfail
    def test_experimental():
        assert False
    
  • 6.2 自定义标记
    • 注册标记
    • 标记分类
    • 按标记过滤
    # pytest.ini
    [pytest]
    markers =
        slow: 标记慢速测试
        integration: 集成测试
        smoke: 冒烟测试
    
    # 测试文件
    @pytest.mark.slow
    def test_slow_operation():
        import time
        time.sleep(2)
        assert True
    
  • 6.3 标记与 Fixture 结合
    • 根据标记使用不同 Fixture
    • 动态 Fixture

七、测试报告与日志

  • 7.1 测试结果输出
    • 控制台输出
    • 详细模式
    • 安静模式
  • 7.2 报告生成
    • 生成 JUnit XML
    • 生成 HTML 报告
    • 生成 Allure 报告
    # 生成 JUnit XML
    pytest --junit-xml=report.xml
    
    # 生成 HTML 报告
    pytest --html=report.html
    
    # 使用 pytest-html
    pytest --html=report.html --self-contained-html
    
  • 7.3 日志记录
    • caplog Fixture
    • 配置日志级别
    • 捕获日志消息
    import logging
    
    def test_logging(caplog):
        caplog.set_level(logging.INFO)
        logging.info("测试日志")
        assert "测试日志" in caplog.text
    
  • 7.4 测试覆盖率
    • pytest-cov 插件
    • 覆盖率报告
    • 覆盖率配置
    
    # 运行测试并计算覆盖率
    pytest --cov=myproject tests/
    
    # 生成 HTML 覆盖率报告
    pytest --cov=myproject --cov-report=html tests/
    

八、Mock 和 Monkeypatch

  • 8.1 什么是 Mock
    • 模拟对象
    • 测试替身
    • 使用场景
  • 8.2 unittest.mock
    • Mock 对象
    • MagicMock
    • patch 装饰器
    from unittest.mock import Mock, patch
    
    def test_mock():
        mock_obj = Mock()
        mock_obj.method.return_value = 42
        assert mock_obj.method() == 42
        mock_obj.method.assert_called_once()
    
    @patch("module.function")
    def test_patch(mock_function):
        mock_function.return_value = "mocked"
        result = module.function()
        assert result == "mocked"
    
  • 8.3 pytest-mock 插件
    • mocker Fixture
    • 简化 Mock
    def test_with_mocker(mocker):
        mock_func = mocker.patch("module.function")
        mock_func.return_value = "mocked"
        result = module.function()
        assert result == "mocked"
        mock_func.assert_called_once()
    
  • 8.4 monkeypatch
    • 临时修改
    • 环境变量
    • 属性修改
    import os
    
    def test_env_var(monkeypatch):
        monkeypatch.setenv("API_KEY", "test_key")
        assert os.getenv("API_KEY") == "test_key"
    
    def test_attribute(monkeypatch):
        class MyClass:
            value = 1
            
        obj = MyClass()
        monkeypatch.setattr(obj, "value", 2)
        assert obj.value == 2
    

九、测试数据管理

  • 9.1 测试数据文件
    • JSON 文件
    • YAML 文件
    • CSV 文件
  • 9.2 Fixture 工厂
    • 动态生成测试数据
    • 数据清理
    @pytest.fixture
    def user_factory():
        users = []
            
        def _create_user(name, age):
            user = {"name": name, "age": age}
            users.append(user)
            return user
            
        yield _create_user
            
        # 清理
        for user in users:
            # 清理逻辑
            pass
    
  • 9.3 测试数据库
    • 测试数据库设置
    • 数据迁移
    • 事务回滚
  • 9.4 临时文件
    • tmp_path Fixture
    • 临时目录
    def test_temp_file(tmp_path):
        d = tmp_path / "sub"
        d.mkdir()
        p = d / "test.txt"
        p.write_text("Hello")
        assert p.read_text() == "Hello"
    

十、插件系统

  • 10.1 常用插件
    • pytest-html: HTML 报告
    • pytest-cov: 测试覆盖率
    • pytest-xdist: 并行测试
    • pytest-ordering: 测试顺序
    • pytest-timeout: 超时控制
    • pytest-asyncio: 异步测试
    • pytest-rerunfailures: 失败重试
  • 10.2 插件安装与配置
    • 安装插件

    • 插件配置
    • 插件选项
    pip install pytest-html pytest-cov pytest-xdist
    pytest --html=report.html --cov=myproject -n 4
    
  • 10.3 自定义插件
    • 钩子函数
    • 命令行选项
    • 自定义 Fixture
    # conftest.py
    def pytest_addoption(parser):
        parser.addoption("--env", action="store", default="test")
    
    @pytest.fixture
    def environment(request):
        return request.config.getoption("--env")
    

十一、并发与并行测试

  • 11.1 pytest-xdist 插件
    • 并行执行
    • 分布式测试
    • CPU 核心数
    # 并行运行测试
    pytest -n auto
    pytest -n 4
    
  • 11.2 并发问题
    • 资源竞争
    • 测试隔离
    • 数据一致性
  • 11.3 优化策略
    • 测试分组
    • 资源管理
    • 测试顺序

十二、异步测试

  • 12.1 pytest-asyncio
    • 异步测试支持
    • async/await
    • 事件循环
    import pytest
    import asyncio
    
    @pytest.mark.asyncio
    async def test_async():
        result = await async_function()
        assert result == "expected"
    
  • 12.2 异步 Fixture
    • 异步设置
    • 异步清理
    @pytest.fixture
    async def async_fixture():
        data = await fetch_data()
        yield data
        await cleanup()
    
  • 12.3 超时控制
    • 测试超时
    • 异步超时
    @pytest.mark.asyncio
    @pytest.mark.timeout(5)
    async def test_timeout():
        await asyncio.sleep(10)  # 会超时
    

十三、测试配置

  • 13.1 配置文件
    • pytest.ini
    • setup.cfg
    • pyproject.toml
    • tox.ini
    # pytest.ini
    [pytest]
    testpaths = tests
    python_files = test_*.py
    python_classes = Test*
    python_functions = test_*
    markers =
        slow: 慢速测试
        integration: 集成测试
    addopts = -v -s
    

    更多关于pytest配置文件

  • 13.2 命令行配置
    • 默认选项
    • 配置文件优先级
    • 13.3 环境配置
    • 环境变量
    • 配置文件
    • 动态配置

十四、测试组织结构

  • 14.1 测试目录结构
    • 扁平结构
    • 分层结构
    • 包结构
    project/
    ├── src/
    │   └── mypackage/
    ├── tests/
    │   ├── unit/
    │   ├── integration/
    │   └── conftest.py
    ├── pytest.ini
    └── pyproject.toml
    
  • 14.2 conftest.py
    • 作用域
    • 共享 Fixture
    • 插件配置
    # tests/conftest.py
    import pytest
    
    @pytest.fixture
    def shared_data():
        return {"key": "value"}
    
    # tests/unit/conftest.py
    @pytest.fixture
    def unit_data():
        return {"unit": "test"}
    
  • 14.3 导入路径
    • sys.path
    • PYTHONPATH
    • 相对导入
  • 14.4 测试分类
    • 单元测试
    • 集成测试
    • 端到端测试
    • 冒烟测试

十五、高级特性

  • 15.1 钩子函数
    • pytest_addoption
    • pytest_configure
    • pytest_collection_modifyitems
    • pytest_runtest_setup
    • pytest_runtest_teardown
    # conftest.py
    def pytest_collection_modifyitems(config, items):
        # 修改测试项顺序
        items.reverse()
            
    def pytest_configure(config):
        # 配置阶段
        config
        .addinivalue_line("markers", "slow: 慢速测试")
    
  • 15.2 自定义断言
    • 断言重写
    • 自定义断言消息
    def test_custom_assertion():
        a = [1, 2, 3]
        b = [1, 2, 3]
        # 自定义断言消息
        assert a == b, f"列表不相等: {a} != {b}"
    
  • 15.3 测试依赖
    • pytest-dependency
    • 测试执行顺序
    import pytest
    
    @pytest.mark.dependency()
    def test_first():
        assert True
    
    @pytest.mark.dependency(depends=["test_first"])
    def test_second():
        assert True
    
  • 15.4 跳过条件
    • 运行时跳过
    • 平台判断
    import sys
    import pytest
    
    @pytest.mark.skipif(sys.platform != "linux", reason="仅在 Linux 上运行")
    def test_linux_only():
        assert True
    

十六、最佳实践

  • 16.1 测试设计原则
    • 单一职责
    • 独立性
    • 可重复性
    • 快速执行
  • 16.2 测试命名规范
    • 描述性名称
    • 统一格式
    • 英文命名
  • 16.3 测试维护
    • 避免重复
    • 及时清理
    • 持续重构
  • 16.4 测试性能
    • 避免 I/O
    • 使用 Mock
    • 并行执行
  • 16.5 测试稳定性
    • 避免随机性
    • 处理竞态条件
    • 资源清理

十七、常见问题与调试

  • 17.1 测试失败分析
    • 回溯信息
    • 断言消息
    • 调试输出
  • 17.2 常见错误
    • Fixture 作用域错误
    • 导入错误
    • 依赖缺失
  • 17.3 调试技巧
    • pdb 调试
    • print 输出
    • 日志记录
    def test_debug():
        import pdb; pdb.set_trace()
        # 或者使用断点
        breakpoint()
    
  • 17.4 测试隔离
    • 环境隔离
    • 数据隔离
    • 进程隔离

十八、CI/CD 集成

  • 18.1 持续集成配置
    • GitHub Actions
    • GitLab CI
    • Jenkins
    • Travis CI
    # .github/workflows/test.yml
    name: Tests
    on: [push, pull_request]
    jobs:
    test:
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v2
        - name: Run tests
            run: |
            pip install -r requirements.txt
            pytest --cov=myproject --cov-report=xml
    
  • 18.2 测试报告集成
    • 测试结果通知
    • 覆盖率报告
    • 测试趋势
  • 18.3 质量门禁
    • 测试通过率
    • 覆盖率阈值
    • 测试时长
    pytest --cov=myproject --cov-fail-under=80
    pytest --junitxml=report.xml
    

十九、学习资源

  • 19.1 官方文档
    • Pytest 官方文档
    • GitHub 仓库
    • 官方示例
  • 19.2 书籍推荐
    • 《Python Testing with pytest》
    • 《Pytest Quick Start Guide》
  • 19.3 在线教程
    • 官方教程
    • 视频课程
    • 博客文章
  • 19.4 社区资源
    • Stack Overflow
    • GitHub Issues
    • 中文社区
  • 19.5 进阶学习
    • 插件开发
    • 源码阅读
    • 贡献代码