pytest:灵活强大的 Python 测试框架

FreeGuideOnline 最新 2026-06-16

Python 测试框架 pytest 完全入门指南

为什么选择 pytest

pytest 是 Python 生态中最受欢迎的测试框架之一。它语法简洁、可扩展性强,支持从简单的单元测试到复杂的功能测试。与 unittest 相比,pytest 无需继承特定类,断言直接使用原生 assert 语句,失败时会提供丰富的上下文信息。

环境准备与安装

pip install pytest

建议在虚拟环境中操作,安装完成后验证版本:

pytest --version

编写第一个测试

pytest 自动发现符合命名规范的文件和函数:测试文件以 test_ 开头或以 _test 结尾,测试函数以 test_ 开头。

创建文件 test_calculator.py

def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0

运行测试:

pytest

pytest 会收集并执行所有测试,输出简洁的结果报告。

强大而直观的断言

pytest 允许使用 Python 原生的 assert 语句,重写了断言重写机制,使得失败时的错误信息更有帮助。

def test_string():
    assert "hello".upper() == "HELLO"

def test_list():
    lst = [1, 2, 3]
    assert len(lst) == 3
    assert lst[0] == 1

def test_dict():
    person = {"name": "Alice", "age": 30}
    assert person["name"] == "Alice"

对于浮点数比较,可使用 pytest.approx

def test_float():
    assert 0.1 + 0.2 == pytest.approx(0.3)

对于异常测试,使用 pytest.raises 上下文管理器:

import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

测试固件 (Fixtures)

fixture 是 pytest 的核心功能,用于提供测试所需的依赖(如数据库连接、测试数据等)。使用 @pytest.fixture 装饰器定义,测试函数将 fixture 名称作为参数即可注入。

import pytest

@pytest.fixture
def sample_data():
    return {"numbers": [1, 2, 3, 4], "name": "test"}

def test_sum(sample_data):
    assert sum(sample_data["numbers"]) == 10

def test_name(sample_data):
    assert sample_data["name"] == "test"

fixture 作用域

通过 scope 参数控制 fixture 的生命周期:function(默认,每个测试函数执行一次)、classmodulepackagesession

@pytest.fixture(scope="module")
def db_connection():
    # 模块中所有测试共享同一个连接
    conn = create_connection()
    yield conn
    conn.close()

使用 yield 可以在 fixture 中实现 setup 和 teardown 逻辑。

参数化 Fixture

fixture 可以间接参数化,但更常见的测试参数化直接在测试函数上使用 @pytest.mark.parametrize

参数化测试

同一测试逻辑使用多组不同数据运行,避免重复代码。

import pytest

def multiply(a, b):
    return a * b

@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 6),
    (0, 5, 0),
    (-2, -3, 6),
    (1.5, 2, 3)
])
def test_multiply(a, b, expected):
    assert multiply(a, b) == expected

参数可以堆叠多个 parametrize 装饰器,实现组合测试:

@pytest.mark.parametrize("x", [0, 1])
@pytest.mark.parametrize("y", [2, 3])
def test_combination(x, y):
    assert x + y == x + y

上述将生成 4 个测试用例 (0,2), (0,3), (1,2), (1,3)。

使用 Markers 标记测试

pytest 提供内置标记,也可自定义标记,用于筛选或跳过测试。

跳过测试

@pytest.mark.skip(reason="功能未实现")
def test_unfinished():
    pass

@pytest.mark.skipif(not hasattr(math, "isclose"), reason="需要 math.isclose")
def test_using_isclose():
    assert math.isclose(0.1 + 0.2, 0.3)

预期失败

@pytest.mark.xfail(raises=ZeroDivisionError)
def test_expected_failure():
    1 / 0

自定义标记

pytest.inipyproject.tomltox.ini 中注册标记:

[pytest]
markers =
    slow: marks tests as slow (deselect with '-m "not slow"')
    integration: integration tests

然后使用:pytest -m "slow" 只运行慢测试,-m "not slow" 跳过慢测试。

测试发现与运行控制

指定测试路径

pytest tests/
pytest tests/test_sample.py::test_function
pytest tests/test_sample.py::TestClass::test_method

常用选项

  • -v:详细输出
  • -s:允许打印输出(不捕获)
  • -k:按关键字表达式筛选测试
pytest -k "add or multiply"  # 运行名称中包含 add 或 multiply 的测试
  • --lf:只运行上次失败的测试
  • --ff:先运行上次失败的测试,再运行其他
  • -x:遇到第一个失败时停止
  • --maxfail=N:失败 N 次后停止

测试覆盖率报告

安装 pytest-cov 插件:

pip install pytest-cov

运行并生成覆盖率:

pytest --cov=my_module tests/
pytest --cov=my_module --cov-report=html

会在 htmlcov 目录生成可视化报告。

常用插件推荐

  • pytest-django:Django 项目测试支持
  • pytest-asyncio:异步代码测试
  • pytest-xdist:分布式执行测试,加速 CI
  • pytest-timeout:设置测试超时时间
  • pytest-mock:简化使用 unittest.mock 的 fixture

组织大型测试项目

推荐目录结构:

project/
├── src/
│   └── calculator.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py          # 共享 fixture
│   ├── test_calculator.py
│   └── integration/
│       └── test_api.py
└── pytest.ini               # 配置文件

conftest.py 用于放置共享的 fixture 和插件配置,pytest 会自动加载同目录及父目录的 conftest。

实战示例:测试一个用户管理模块

假设 user.py

class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def is_adult(self):
        return self.age >= 18

测试文件 test_user.py 使用 fixture 和参数化:

import pytest
from user import User

@pytest.fixture
def default_user():
    return User("Bob", 25)

def test_user_creation(default_user):
    assert default_user.name == "Bob"
    assert default_user.age == 25

@pytest.mark.parametrize("age, expected", [
    (17, False),
    (18, True),
    (21, True)
])
def test_is_adult(age, expected):
    user = User("Test", age)
    assert user.is_adult() == expected

总结

pytest 凭借其零模板的编写方式、强大的 fixture 管理和丰富的插件生态,成为 Python 测试的首选。掌握上述核心特性后,你可以轻松构建可维护、高效的测试套件,并将其集成到持续集成流水线中。开始编写你的第一个 pytest 测试,让代码质量更有保障。