1. 项目概述:为什么是pytest?

如果你刚开始接触Python自动化测试,或者已经用了一段时间的 unittest ,现在想找一个更强大、更现代的工具,那么 pytest 几乎是你绕不开的选择。我最初从 unittest 切换到 pytest ,纯粹是因为受不了那些繁琐的 self.assert* 方法和必须继承 TestCase 类的约束,写个简单的测试感觉像是在写八股文。用了 pytest 之后,最大的感受就是“自由”——你可以用普通的Python函数来写测试,断言失败时能直接看到具体的值对比,而不是一个冷冰冰的 AssertionError 。它几乎不需要任何样板代码,却能通过丰富的插件生态实现从单元测试到复杂的功能测试、性能测试的全覆盖。对于新手来说, pytest 的学习曲线非常平缓,核心概念清晰;对于老手,它又提供了足够的深度和灵活性去构建复杂的测试套件。这篇指南的目的,就是帮你跨越从“知道pytest这个名字”到“能熟练用它编写和维护测试”的鸿沟,我会结合自己踩过的坑和总结的最佳实践,把它的核心概念和用法掰开揉碎了讲清楚。

2. 环境搭建与第一个测试

2.1 安装pytest与IDE配置

安装 pytest 非常简单,一条 pip 命令即可。但这里有个细节:强烈建议在虚拟环境中进行。无论是用 venv virtualenv 还是 conda ,虚拟环境能隔离项目依赖,避免版本冲突。打开你的终端或命令行,执行:

pip install pytest

安装完成后,可以通过 pytest --version 来验证。你会看到类似 pytest 7.4.3 的输出,表明安装成功。

接下来是编辑器的选择。PyCharm对 pytest 的支持是开箱即用的,你几乎不需要额外配置。如果你使用VS Code,则需要安装Python扩展,并且建议在项目根目录的 .vscode/settings.json 文件中进行一些配置,以优化测试体验:

{
    "python.testing.pytestEnabled": true,
    "python.testing.unittestEnabled": false,
    "python.testing.pytestArgs": [
        "tests" // 指定你的测试目录
    ]
}

这个配置告诉VS Code使用 pytest 作为测试框架,并默认在 tests 目录下寻找测试文件。设置好后,你可以在侧边栏的测试视图中直接发现、运行和调试测试用例,非常方便。

2.2 编写与运行你的第一个测试

pytest 寻找测试文件的规则很智能:它会递归查找当前目录及子目录下所有以 test_ 开头或以 _test.py 结尾的文件。在这些文件中,它会识别所有以 test_ 开头的函数,以及以 Test 开头的类中所有以 test_ 开头的方法。

让我们创建一个最简单的测试。在你的项目根目录下,新建一个文件 test_sample.py

# test_sample.py

def test_addition():
    assert 1 + 1 == 2

def test_failure_example():
    # 这个测试会失败,让我们看看pytest如何报告错误
    result = 10 / 2
    assert result == 4  # 实际上 result 是 5.0

在终端中,切换到项目根目录,直接运行 pytest 命令:

pytest

你会看到类似以下的输出:

============================= test session starts ==============================
platform darwin -- Python 3.9.0, pytest-7.4.3, pluggy-1.3.0
rootdir: /path/to/your/project
collected 2 items

test_sample.py .F                                                         [100%]

=================================== FAILURES ===================================
_____________________________ test_failure_example _____________________________

    def test_failure_example():
        result = 10 / 2
>       assert result == 4
E       assert 5.0 == 4

test_sample.py:8: AssertionError
=========================== short test summary info ============================
FAILED test_sample.py::test_failure_example - assert 5.0 == 4
========================= 1 failed, 1 passed in 0.02s =========================

这个输出信息量很大:

  1. 测试会话信息 :展示了Python版本、pytest版本、项目根目录。
  2. 测试收集 collected 2 items 表示找到了两个测试函数。
  3. 进度与结果 .F 中,点( . )表示通过, F 表示失败。 [100%] 是进度条。
  4. 详细的失败报告 :这是 pytest 的杀手锏。它不仅告诉你断言失败了,还直接输出了表达式两边的值( assert 5.0 == 4 ),让你一眼就能看出问题所在,无需像 unittest 那样去猜测 AssertionError 到底是什么意思。
  5. 总结 :清晰地列出了哪个测试失败了,以及失败原因。

注意 pytest 的断言之所以如此强大,是因为它重写了Python内置的 assert 语句。在 unittest 中,你需要使用 self.assertEqual(a, b) 这样的方法,而 pytest 让你可以直接使用更符合直觉的 assert a == b 。当断言失败时, pytest 会利用其内省(introspection)能力,计算出表达式中各个部分的值并展示出来,极大地方便了调试。

3. 核心概念深度解析

3.1 Fixture:测试的基石与依赖注入

如果说 pytest 只有一个核心概念必须掌握,那一定是 Fixture 。你可以把它理解为一个“测试夹具”,用于为测试函数提供预设的、可重用的上下文或数据。它的核心思想是 依赖注入 ,将测试所需的资源(如数据库连接、临时文件、API客户端)的创建和清理逻辑与测试逻辑本身解耦。

定义一个 Fixture 非常简单,使用 @pytest.fixture 装饰器即可。看一个最经典的例子——数据库连接:

# conftest.py 或任何测试文件中
import pytest
import sqlite3
from myapp.models import create_tables, drop_tables

@pytest.fixture
def database_connection():
    """提供一个临时的、内存中的SQLite数据库连接。"""
    # 1. Setup: 创建资源
    conn = sqlite3.connect(':memory:') # 使用内存数据库,测试互不干扰
    create_tables(conn) # 创建表结构
    print("\n[Fixture] 数据库连接已建立,表已创建")
    
    # 将资源“yield”给测试函数使用
    yield conn
    
    # 3. Teardown: 清理资源 (在yield之后执行)
    drop_tables(conn)
    conn.close()
    print("[Fixture] 数据库连接已关闭,表已删除")

# 测试文件 test_db.py
def test_insert_user(database_connection): # Fixture通过函数参数注入
    cursor = database_connection.cursor()
    cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
    database_connection.commit()
    
    cursor.execute("SELECT name FROM users WHERE name='Alice'")
    result = cursor.fetchone()
    assert result is not None
    assert result[0] == 'Alice'

关键点解析:

  1. yield vs return Fixture 使用 yield 来分隔“设置”和“清理”阶段。 yield 之前的代码在测试函数 之前 运行, yield 返回的值(这里是 conn )会注入到测试函数中。测试函数执行完毕后,会回到 Fixture 中,执行 yield 之后的清理代码。如果使用 return ,则无法执行清理操作。
  2. 作用域(Scope) Fixture 默认的作用域是 function ,即每个测试函数都会调用一次。你可以通过 @pytest.fixture(scope="module") scope="session" 来改变。 module 作用域表示同一个 .py 文件中的所有测试共享一个 Fixture 实例; session 作用域则表示整个测试会话(一次 pytest 命令执行)只创建一次。合理使用作用域可以大幅提升测试速度,尤其是对于创建成本高的资源(如启动浏览器、建立数据库连接池)。
  3. conftest.py 文件 :这是一个特殊的文件。 pytest 会自动发现项目目录树中所有名为 conftest.py 的文件,并将其中的 Fixture 提供给该目录及其所有子目录中的测试文件使用 。这是组织共享 Fixture 的最佳实践。比如,将 database_connection 放在项目根目录的 conftest.py 中,所有子目录的测试都能用到它。

实操心得 :不要滥用 session 作用域。虽然它能提速,但如果测试会修改 Fixture 返回的对象状态(比如向共享的数据库连接插入数据),可能会导致测试间相互污染,变得不稳定。一个原则是: 只读的、无状态的资源适合用更大作用域;会被修改的资源,最好用 function 作用域 。对于数据库,我通常使用 function 作用域配合事务回滚( rollback )来确保每个测试的独立性。

3.2 参数化测试:用数据驱动测试

当你需要对同一个测试逻辑,用多组不同的输入和预期输出进行验证时, @pytest.mark.parametrize 装饰器就是你的最佳工具。它避免了编写大量重复的测试函数。

假设我们要测试一个字符串处理函数 reverse_string(s)

import pytest

def reverse_string(s):
    return s[::-1]

# 传统方式:写多个测试函数
def test_reverse_hello():
    assert reverse_string("hello") == "olleh"

def test_reverse_empty():
    assert reverse_string("") == ""

def test_reverse_palindrome():
    assert reverse_string("radar") == "radar"

# 使用参数化的优雅方式
@pytest.mark.parametrize("input_str, expected", [
    ("hello", "olleh"),
    ("", ""),
    ("radar", "radar"),
    ("a", "a"),
    ("12345", "54321"),
])
def test_reverse_string_parametrized(input_str, expected):
    assert reverse_string(input_str) == expected

运行测试时, pytest 会将这组参数展开,变成5个独立的测试用例来执行,并在报告中清晰显示每个参数组合的结果。

高级用法:参数化与Fixture结合 你可以将参数化应用到 Fixture 上,实现更动态的资源创建。例如,为测试提供不同的数据库配置:

import pytest

@pytest.fixture(params=['sqlite', 'postgresql'])
def database_config(request):
    """参数化Fixture,提供不同的数据库配置。"""
    configs = {
        'sqlite': {'driver': 'sqlite', 'database': ':memory:'},
        'postgresql': {'driver': 'pg', 'database': 'test_db', 'host': 'localhost'}
    }
    config = configs[request.param]
    print(f"\n使用数据库配置: {config}")
    yield config
    print(f"清理 {config['driver']} 配置")

def test_db_operations(database_config):
    # 这个测试会运行两次,分别使用sqlite和postgresql配置
    assert 'driver' in database_config
    # 这里可以根据不同的driver编写适配的测试逻辑

在这个例子中, test_db_operations 会执行两次,每次 database_config Fixture 会提供不同的配置字典。 request 是一个内建的 Fixture ,可以访问当前测试的上下文信息,其中 request.param 就是传入的参数值。

3.3 Mark:给测试打标签与分类

Mark (标记)用于给测试函数或类添加元数据,从而实现测试的分类、筛选和特殊处理。最常用的内置标记是 @pytest.mark.skip (跳过测试)和 @pytest.mark.xfail (预期失败)。

import pytest
import sys

@pytest.mark.skip(reason="此功能在v2.0中尚未实现")
def test_new_feature():
    assert False

@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8及以上版本")
def test_f_string_feature():
    # 使用了3.8的某些特性
    name = "World"
    assert f"Hello {name=}" == "Hello name='World'"

@pytest.mark.xfail(reason="已知Bug #123,下个版本修复")
def test_buggy_function():
    # 这个测试目前会失败,但我们预期它失败
    assert some_buggy_function() == "expected_result"

def test_normal_function():
    assert True

自定义标记与筛选 你可以定义自己的标记来对测试进行分组,例如 @pytest.mark.slow (慢速测试)、 @pytest.mark.integration (集成测试)。

# conftest.py 中注册自定义标记,避免pytest警告
def pytest_configure(config):
    config.addinivalue_line("markers", "slow: 标记运行缓慢的测试")
    config.addinivalue_line("markers", "integration: 集成测试,需要外部服务")

# test_suite.py
import time
import pytest

@pytest.mark.slow
def test_complex_calculation():
    time.sleep(2) # 模拟耗时操作
    assert 1 == 1

@pytest.mark.integration
def test_api_integration():
    # 调用真实的外部API
    assert call_external_api() is not None

def test_fast_unit():
    assert True

运行测试时,可以使用 -m 选项来筛选:

  • pytest -m slow :只运行标记为 slow 的测试。
  • pytest -m "not slow" :运行所有 slow 的测试。
  • pytest -m "integration and not slow" :运行是 integration 但不是 slow 的测试。

这对于在持续集成(CI)流水线中区分快速单元测试和慢速集成测试非常有用,可以加快日常开发反馈循环。

4. 高级特性与实战技巧

4.1 插件系统:扩展pytest的能力

pytest 的强大很大程度上得益于其丰富的插件生态系统。你可以通过 pip 安装社区插件来获得各种额外功能。以下是一些“必备”插件:

  1. pytest-cov : 生成测试覆盖率报告。

    pip install pytest-cov
    pytest --cov=my_package tests/ --cov-report=html
    

    这会在 htmlcov 目录下生成一个漂亮的HTML报告,直观地展示哪些代码被测试覆盖了。

  2. pytest-xdist : 并行运行测试,显著缩短大型测试套件的执行时间。

    pip install pytest-xdist
    pytest -n auto # 使用与CPU核心数相同的worker并行运行
    
  3. pytest-mock : 集成了 unittest.mock ,提供了 mocker Fixture ,让模拟(Mock)和打桩(Stub)更简单。

    import pytest
    
    def test_with_mock(mocker):
        # mocker是pytest-mock提供的Fixture
        mock_requests = mocker.patch('my_module.requests.get')
        mock_requests.return_value.status_code = 200
        mock_requests.return_value.json.return_value = {'key': 'value'}
        
        result = my_module.fetch_data()
        assert result == {'key': 'value'}
        mock_requests.assert_called_once_with('https://api.example.com/data')
    
  4. pytest-html : 生成美观的HTML测试报告。

    pip install pytest-html
    pytest --html=report.html
    

注意事项 :插件虽好,但不要过度依赖。在引入一个新插件前,先问问自己:这个功能是否真的必要?是否可以通过编写自定义 Fixture 或使用 pytest 原生功能实现?过多的插件会增加项目的复杂性和维护成本。

4.2 测试发现与自定义配置

pytest 的测试发现规则可以通过 pytest.ini pyproject.toml setup.cfg 文件进行自定义。 pytest.ini 是最常用的方式。

一个典型的 pytest.ini 文件配置如下:

[pytest]
# 指定测试文件的搜索模式
testpaths = tests unit_tests integration_tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*

# 添加命令行默认选项
addopts = -v --tb=short --strict-markers

# 注册自定义标记,避免使用未注册标记时出现警告
markers =
    slow: marks tests as slow (deselect with '-m \"not slow\"')
    integration: marks tests as integration tests (require external services)
    smoke: quick smoke tests

# 设置最低的pytest版本要求
minversion = 6.0

# 配置特定插件的选项
# 例如,为pytest-cov配置
[tool:pytest]
# 注意:pytest-cov的配置有时在 [tool:pytest] 下,取决于版本和配置文件类型
# 在 pyproject.toml 中配置更现代

--tb 选项 :控制断言失败时的回溯(traceback)信息详细程度。 --tb=short 只显示失败位置的简短回溯,非常清晰; --tb=line 只显示一行摘要; --tb=no 则不显示回溯。在CI环境中,为了日志简洁,我通常使用 --tb=short

4.3 断言与异常测试

除了简单的 assert pytest 还提供了一些用于更复杂断言场景的辅助函数,它们位于 pytest 包中(但需要单独导入 pytest )。

import pytest

def test_approximate_float():
    # 比较浮点数,避免精度问题
    assert 0.1 + 0.2 == pytest.approx(0.3)

def test_collection_contents():
    expected = [1, 2, 3]
    result = some_function()
    # 检查列表是否包含特定元素,顺序不重要
    assert set(result) == set(expected)
    # 或者使用pytest的unordered检查(需要pytest 7.0+)
    # assert result == pytest.unordered(expected)

def test_exception():
    # 测试代码是否抛出了预期的异常
    with pytest.raises(ValueError) as exc_info:
        int('not_a_number')
    
    # 可以进一步检查异常信息
    assert exc_info.value.args[0] == "invalid literal for int() with base 10: 'not_a_number'"
    # 或者使用 match参数进行正则匹配
    with pytest.raises(ValueError, match="invalid literal for int.*"):
        int('not_a_number')

pytest.approx() 在测试涉及浮点数计算时是必不可少的,因为直接使用 == 比较浮点数往往会因为精度问题而失败。

5. 构建可维护的测试套件

5.1 测试代码的组织结构

一个清晰的项目结构对测试的可维护性至关重要。以下是一个推荐的结构:

my_project/
├── src/                    # 源代码
│   └── my_package/
│       ├── __init__.py
│       ├── module_a.py
│       └── module_b.py
├── tests/                  # 测试代码
│   ├── __init__.py        # 可选,使tests成为一个包
│   ├── conftest.py        # 项目级的共享Fixture
│   ├── unit/              # 单元测试
│   │   ├── __init__.py
│   │   ├── conftest.py    # 单元测试专用的Fixture
│   │   ├── test_module_a.py
│   │   └── test_module_b.py
│   └── integration/       # 集成测试
│       ├── __init__.py
│       ├── conftest.py    # 集成测试专用的Fixture(如启动docker容器)
│       └── test_api.py
├── pyproject.toml         # 项目依赖和配置(现代Python项目标准)
└── pytest.ini             # pytest配置

关键点:

  • src 布局 :将源代码放在 src 目录下,可以避免在导入时无意中引入当前目录下的其他模块,让导入行为更清晰、一致。
  • 测试目录镜像源码结构 tests/unit/ 下的文件结构尽量与 src/my_package/ 下的结构对应,这样找测试用例会很方便。
  • 分层 conftest.py Fixture 的作用域遵循就近原则。你可以把最通用的 Fixture (如日志配置)放在项目根目录的 conftest.py 中;把单元测试专用的 Fixture (如模拟对象)放在 tests/unit/conftest.py 中;把集成测试需要的昂贵资源 Fixture (如测试数据库)放在 tests/integration/conftest.py 中。

5.2 编写高质量测试的原则

  1. 独立性(Isolation) :每个测试应该独立运行,不依赖其他测试的状态或顺序。这是 pytest 默认以随机顺序运行测试的原因。确保你的 Fixture (特别是 function 作用域的)能正确地进行 setup teardown
  2. 快速(Fast) :测试套件应该能快速执行,以便频繁运行。将慢速测试(如调用外部API、读写大量文件)标记为 @pytest.mark.slow ,并在日常开发中排除它们。
  3. 可读性(Readable) :测试函数名应该清晰地描述其行为。好的模式是 test_<被测试函数>_<输入状态>_<预期结果> ,例如 test_withdraw_money_with_insufficient_balance_raises_error 。虽然长,但一目了然。
  4. 测试行为,而非实现(Test Behavior, Not Implementation) :测试应该关注函数或模块对外表现出的行为(输入输出),而不是其内部实现细节。这样当内部实现重构时,只要行为不变,测试就无需修改。
  5. 使用恰当的断言粒度 :一个测试函数最好只验证一个逻辑概念。但这不意味着只能有一个 assert 语句。多个相关的 assert 语句用于验证同一个概念的多个方面是可以的,例如在测试一个返回字典的函数时,可以连续断言字典的键、值和类型。

5.3 测试数据的管理

测试数据的管理是个常见痛点。硬编码在测试函数里会让测试变得冗长,散落在各处又难以维护。

策略一:使用 Fixture 返回数据 对于简单的、静态的数据,直接在 Fixture 中构造并返回。

@pytest.fixture
def sample_user_data():
    return {
        'username': 'test_user',
        'email': 'user@example.com',
        'is_active': True
    }

策略二:使用外部数据文件 对于复杂或大量的测试数据(如JSON、CSV、YAML),将其存放在单独的文件中。

tests/
├── data/
│   ├── users.json
│   └── products.csv
└── conftest.py
# conftest.py
import json
import pytest
import os

@pytest.fixture
def user_data():
    data_file = os.path.join(os.path.dirname(__file__), 'data', 'users.json')
    with open(data_file, 'r') as f:
        return json.load(f)

# 在测试中使用
def test_user_creation(user_data):
    for user in user_data:
        # 用每一条数据驱动测试
        assert is_valid_username(user['username'])

策略三:使用工厂函数(Factory) 当需要创建多个类似但略有不同的对象时(比如测试需要不同状态的用户),使用工厂模式。

# tests/factories.py
class UserFactory:
    @staticmethod
    def create_user(username=None, email=None, is_active=True):
        """返回一个用户字典,可以覆盖默认值。"""
        return {
            'username': username or f'user_{uuid.uuid4().hex[:8]}',
            'email': email or f'{username}@example.com',
            'is_active': is_active
        }

# 在测试或Fixture中使用
def test_active_user():
    user = UserFactory.create_user(is_active=True)
    assert user['is_active'] is True

def test_inactive_user():
    user = UserFactory.create_user(is_active=False)
    assert user['is_active'] is False

6. 常见问题与排查技巧实录

6.1 Fixture作用域与测试污染

问题 :测试时有时成功有时失败,看起来像是测试之间相互影响了状态。 排查 :这通常是 Fixture 作用域使用不当导致的。一个 session module 作用域的 Fixture ,如果被一个测试修改了,那么后续使用同一个 Fixture 实例的测试就会看到一个被污染的状态。 解决

  1. 检查你的 Fixture 作用域。对于会被测试修改的资源(如数据库连接、内存中的列表/字典),优先使用 function 作用域。
  2. 如果必须使用大作用域 Fixture (如启动一个很慢的Docker容器),确保每个测试开始前都将其状态重置到一个已知的干净状态。可以在 Fixture 中使用 yield ,并在 yield 之后不做清理,而是在每个测试开始时调用一个专门的 reset 函数(这个函数本身也可以是一个 Fixture )。
@pytest.fixture(scope="session")
def expensive_external_service():
    service = start_service() # 启动很慢
    yield service
    service.shutdown() # 会话结束时关闭

@pytest.fixture
def clean_service(expensive_external_service):
    # 每个测试函数运行前,都重置服务状态
    expensive_external_service.reset_to_initial_state()
    yield expensive_external_service
    # 这里通常不需要额外的teardown,因为service本身是session作用域

6.2 导入错误(ImportError)与路径问题

问题 :运行 pytest 时提示 ModuleNotFoundError: No module named 'my_package' 排查 :这通常是因为Python的模块搜索路径( sys.path )中没有包含你的源代码目录。当你直接在项目根目录运行 pytest 时,Python可能会将当前目录( . )加入路径,但如果你的代码在 src 目录下,就可能找不到。 解决

  1. 推荐方案 :使用 src 布局,并通过 pip install -e . 以可编辑模式安装你的包。这样你的包就像第三方包一样存在于Python环境中,任何地方都能导入。
  2. 临时方案 :在 pytest 运行时修改 sys.path 。可以在项目根目录的 conftest.py 文件中添加:
# conftest.py
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))

但这种方法不够优雅,且可能在其他工具(如IDE)中失效。第一种方法是更标准的做法。

6.3 测试跳过(skip)与失败(xfail)的管理

问题 :标记为 @pytest.mark.xfail 的测试如果意外通过了,或者跳过的测试太多,会干扰对测试套件健康度的判断。 解决

  • 使用 pytest --strict-markers 确保所有使用的标记都在 pytest.ini 中注册过,避免拼写错误。
  • 定期(比如每个冲刺周期结束前)审查被跳过的测试( @pytest.mark.skip )。问自己:这个测试为什么还被跳过?相关的功能实现了吗?如果实现了,就移除 skip 标记;如果功能被移除了,就连同测试代码一起删除。
  • 对于 xfail 测试,使用 strict=True 参数。 @pytest.mark.xfail(strict=True) 表示这个测试 必须失败 ,如果它通过了, pytest 会将其报告为一个 FAIL (而不是默认的 XPASS )。这能迫使你关注那些已经修复但标记未更新的测试。
@pytest.mark.xfail(reason="Bug #456", strict=True)
def test_fixed_bug():
    # 这个bug已经被修复了,所以测试会通过
    result = fixed_function()
    assert result == "expected"
# 运行后,这会是一个FAIL,提醒你去掉xfail标记。

6.4 测试执行速度优化

当测试套件变得庞大时,执行速度会成为瓶颈。以下是一些优化策略:

  1. 使用 pytest-xdist 并行运行 :这是提升速度最直接有效的方法,尤其适合CPU密集或I/O等待多的测试。
  2. 合理使用 Fixture 作用域 :将创建成本高的资源(如数据库连接、HTTP会话)设置为 session module 作用域,避免重复创建。
  3. 使用 --lf --ff 选项
    • pytest --lf (last-failed): 只重新运行上次失败的测试。
    • pytest --ff (failed-first): 先运行上次失败的测试,然后再运行其他的。 这在修复bug时非常高效。
  4. 区分快慢测试 :用自定义标记(如 slow )标记耗时长的测试。在本地开发时,使用 pytest -m "not slow" 只运行快速测试。在CI流水线中,可以分阶段运行:先运行所有非慢速测试,快速反馈;再在一个单独的、允许更长时间的任务中运行慢速测试。
  5. Mock外部依赖 :对于调用网络API、数据库、文件系统的测试,使用 pytest-mock 进行模拟,将I/O操作替换为内存中的模拟对象,速度会有数量级的提升。

我个人习惯在项目的 pytest.ini 中配置 addopts = -x --tb=short -m "not slow" -x 表示遇到第一个失败就停止, --tb=short 提供简洁的错误回溯, -m "not slow" 默认跳过慢速测试。这样在本地运行 pytest 时,能获得最快的反馈循环。完整的测试套件(包括慢速测试)则交给CI系统在每次提交或合并时去执行。

更多推荐