跳转至

Pytest测试框架 — Python Pytest测试入门与进阶

一句话概述:Pytest 是 Python 最流行的测试框架,用简单的 assert 语句替代复杂的 self.assertEqual,支持参数化、fixture、插件,让写测试像写普通函数一样轻松。

核心知识点速查表

概念白话解释
assert 断言就是"我断定这个结果应该是XX",不对就报错
fixture测试的"准备工作",比如创建数据库连接、准备测试数据
参数化一个测试函数,自动跑多组不同数据
conftest.py放公共 fixture 的文件,同目录下的测试自动能用
marker 标记给测试打标签,比如标记为"慢测试",可以单独跑或跳过
插件扩展 pytest 功能的组件,比如 pytest-cov 算覆盖率

当前版本信息(2026年)

信息详情
最新版本Pytest 9.0.2(2026年4月7日)
Python 要求Python 3.9+
官网https://docs.pytest.org
GitHubhttps://github.com/pytest-dev/pytest
内置规则数850+ 外部插件

安装配置

基本安装

# 用 pip 安装 pytest(推荐用虚拟环境)
pip install pytest

# 用 uv 安装(更快)
uv pip install pytest

# 验证安装成功
pytest --version  # 应该显示 pytest 9.0.x

常用插件安装

# 覆盖率插件(看你的代码被测了多少)
pip install pytest-cov

# 异步测试支持(测试 async 函数用的)
pip install pytest-asyncio

# HTML 测试报告(生成漂亮的网页报告)
pip install pytest-html

# 并行执行(多个测试同时跑,加速)
pip install pytest-xdist

# mock 模拟(模拟外部依赖,比如数据库、API)
pip install pytest-mock

配置文件(pyproject.toml)

# 在 pyproject.toml 中配置 pytest(推荐方式)
[tool.pytest.ini_options]
minversion = "9.0"           # 最低 pytest 版本要求
testpaths = ["tests"]         # 测试文件放在 tests 目录
python_files = "test_*.py"    # 测试文件命名规则
python_functions = "test_*"   # 测试函数命名规则
addopts = "-v --tb=short"     # 默认参数:详细模式 + 简短回溯
markers = [                   # 自定义标记
    "slow: 运行慢的测试",
    "integration: 集成测试",
]

基本使用

第一个测试

# tests/test_calculator.py
# 测试文件必须以 test_ 开头

def add(a, b):
    """简单的加法函数"""
    return a + b  # 返回两数之和

def test_add_positive():
    """测试正数相加"""
    result = add(1, 2)  # 调用被测函数
    assert result == 3  # 断言结果应该等于3

def test_add_negative():
    """测试负数相加"""
    assert add(-1, -2) == -3  # 负数相加

def test_add_zero():
    """测试加零"""
    assert add(5, 0) == 5  # 任何数加零等于自身

运行测试

# 运行所有测试(在项目根目录执行)
pytest

# 运行指定文件
pytest tests/test_calculator.py

# 运行指定函数
pytest tests/test_calculator.py::test_add_positive

# 详细输出(显示每个测试的名字和结果)
pytest -v

# 遇到第一个失败就停止(调试时很有用)
pytest -x

# 只运行上次失败的测试
pytest --lf

# 显示 print 输出(默认被 pytest 捕获了)
pytest -s

Fixture(测试准备工作)

# tests/conftest.py
# conftest.py 放公共 fixture,同目录所有测试文件自动可用
import pytest

@pytest.fixture  # 装饰器声明这是一个 fixture
def sample_data():
    """准备测试数据"""
    return {  # 返回一个字典作为测试数据
        "name": "张三",
        "age": 25,
        "scores": [85, 92, 78]
    }

@pytest.fixture
def temp_file(tmp_path):
    """创建临时测试文件(tmp_path 是 pytest 内置 fixture)"""
    file = tmp_path / "test.txt"  # 在临时目录创建文件
    file.write_text("hello world")  # 写入测试内容
    return file  # 返回文件路径给测试使用

# tests/test_data.py
def test_name(sample_data):
    """fixture 会自动注入到参数中"""
    assert sample_data["name"] == "张三"  # 使用 fixture 提供的数据

def test_scores_avg(sample_data):
    """测试平均分计算"""
    scores = sample_data["scores"]  # 取出分数列表
    avg = sum(scores) / len(scores)  # 计算平均分
    assert avg == 85.0  # 断言平均分

参数化测试

import pytest

# @pytest.mark.parametrize 让一个测试函数跑多组数据
@pytest.mark.parametrize("input_val, expected", [
    (1, 1),      # 1的阶乘是1
    (2, 2),      # 2的阶乘是2
    (3, 6),      # 3的阶乘是6
    (4, 24),     # 4的阶乘是24
    (5, 120),    # 5的阶乘是120
])
def test_factorial(input_val, expected):
    """测试阶乘函数,5组数据会产生5个独立测试"""
    from math import factorial  # 导入阶乘函数
    assert factorial(input_val) == expected  # 断言每组结果

高级用法

Fixture 作用域与清理

import pytest

@pytest.fixture(scope="session")  # session 级别:整个测试过程只执行一次
def db_connection():
    """模拟数据库连接(整个测试过程只连一次)"""
    print(">>> 连接数据库")
    conn = {"host": "localhost", "connected": True}  # 模拟连接
    yield conn  # yield 之前是 setup,之后是 teardown
    print(">>> 断开数据库")  # 所有测试结束后执行清理
    conn["connected"] = False

@pytest.fixture(scope="function")  # function 级别:每个测试函数都执行(默认)
def clean_table(db_connection):
    """每个测试前清空表"""
    print("清空测试表")
    yield  # 测试执行
    print("回滚数据")  # 测试结束后回滚

# scope 的4个级别(从小到大):
# function  — 每个测试函数执行一次(默认)
# class     — 每个测试类执行一次
# module    — 每个测试文件执行一次
# session   — 整个测试过程执行一次

异常测试

import pytest

def divide(a, b):
    """除法函数"""
    if b == 0:
        raise ValueError("除数不能为零")  # 除数为零时抛异常
    return a / b

def test_divide_by_zero():
    """测试除以零应该抛出 ValueError"""
    with pytest.raises(ValueError) as exc_info:  # 捕获预期的异常
        divide(10, 0)  # 这行应该抛异常
    assert "除数不能为零" in str(exc_info.value)  # 检查异常消息

def test_divide_normal():
    """测试正常除法"""
    assert divide(10, 2) == 5.0  # 正常情况应该返回结果

Mock 模拟

# tests/test_api.py
from unittest.mock import patch, MagicMock

def fetch_user(user_id):
    """假设这个函数会调用真实的 API"""
    import requests
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()

def test_fetch_user_mock():
    """用 mock 模拟 API 调用,不需要真的发网络请求"""
    mock_response = MagicMock()  # 创建模拟对象
    mock_response.json.return_value = {"id": 1, "name": "测试用户"}  # 设置返回值

    with patch("requests.get", return_value=mock_response):  # 替换 requests.get
        result = fetch_user(1)  # 调用被测函数(不会真的发请求)
        assert result["name"] == "测试用户"  # 验证结果

覆盖率报告

# 运行测试并计算覆盖率
pytest --cov=src --cov-report=html

# --cov=src       指定要统计覆盖率的源码目录
# --cov-report=html  生成 HTML 报告(在 htmlcov/ 目录)
# 打开 htmlcov/index.html 查看详细覆盖率

# 设置最低覆盖率(低于80%则测试失败)
pytest --cov=src --cov-fail-under=80

标记与过滤

import pytest

@pytest.mark.slow  # 标记为慢测试
def test_big_data_processing():
    """处理大量数据的测试(耗时较长)"""
    import time
    time.sleep(2)  # 模拟耗时操作
    assert True

@pytest.mark.skipif(  # 条件跳过
    condition=True,
    reason="功能还没实现"
)
def test_not_ready():
    """这个测试会被跳过"""
    pass

@pytest.mark.xfail(reason="已知 bug")  # 预期失败
def test_known_bug():
    """这个测试预期会失败,不会算作错误"""
    assert 1 + 1 == 3
# 只运行标记为 slow 的测试
pytest -m slow

# 跳过标记为 slow 的测试
pytest -m "not slow"

# 只运行集成测试
pytest -m integration

Pytest 9.x 新特性

# 原生 TOML 配置支持(Pytest 9.0 新增)
# 以前只能在 [tool.pytest.ini_options] 下用 INI 兼容格式
# 现在可以用原生 TOML 格式
[tool.pytest]
minversion = "9.0"
testpaths = ["tests"]

# 终端进度条(9.0 新增,部分终端可用)
# 在终端标签页显示测试进度
# 手动启用:pytest -p terminalprogress

# 核心 Subtests 支持(9.0 新增,原来是插件)
# 一个测试函数内可以有多个子测试

常见报错与解决

报错信息原因解决方案
ModuleNotFoundError找不到被测模块在项目根目录运行,或加 __init__.py,或配置 pythonpath
collected 0 items没找到测试检查文件名是否以 test_ 开头,函数名是否以 test_ 开头
fixture not foundfixture 找不到检查 conftest.py 位置,fixture 名是否拼对
FAILED assert ...断言失败看 pytest 输出的详细对比,找出实际值和期望值的差异
PytestUnhandledCoroutineWarning异步测试没加标记安装 pytest-asyncio 并加 @pytest.mark.asyncio
fixture scope mismatchfixture 作用域冲突低作用域 fixture 不能依赖高作用域的,调整 scope

常见问题处理

# 问题1:ModuleNotFoundError
# 在 pyproject.toml 中添加:
# [tool.pytest.ini_options]
# pythonpath = ["src"]

# 问题2:测试发现不了
# 确保文件结构正确:
# project/
# ├── src/
# │   └── mymodule.py
# ├── tests/
# │   ├── __init__.py      # 这个文件很重要!
# │   └── test_mymodule.py
# └── pyproject.toml

# 问题3:fixture 冲突
# 检查 conftest.py 层级
# project/
# ├── conftest.py          # 全局 fixture
# └── tests/
#     ├── conftest.py      # 测试目录级 fixture
#     ├── unit/
#     │   └── conftest.py  # 单元测试级 fixture
#     └── integration/
#         └── conftest.py  # 集成测试级 fixture

速查表

# ===== 运行命令 =====
pytest                           # 运行所有测试
pytest -v                        # 详细模式
pytest -x                        # 遇到失败立即停止
pytest -s                        # 显示 print 输出
pytest --lf                      # 只跑上次失败的
pytest --ff                      # 先跑上次失败的,再跑其他
pytest -k "add"                  # 按名字过滤(包含 add 的)
pytest -m slow                   # 按标记过滤
pytest --co                      # 只列出测试,不运行
pytest -n auto                   # 并行运行(需要 pytest-xdist)
pytest --cov=src                 # 统计覆盖率

# ===== 常用断言 =====
assert x == y                    # 相等
assert x != y                    # 不等
assert x > y                     # 大于
assert x in collection           # 包含
assert isinstance(x, int)        # 类型检查
assert not x                     # 为假
pytest.raises(ValueError)        # 预期抛异常
pytest.approx(0.1 + 0.2, 0.3)   # 浮点数近似相等

# ===== Fixture 装饰器 =====
@pytest.fixture                  # 函数级 fixture
@pytest.fixture(scope="session") # 会话级 fixture
@pytest.fixture(autouse=True)    # 自动使用(不需要参数注入)
@pytest.fixture(params=[1,2,3])  # 参数化 fixture

# ===== 标记装饰器 =====
@pytest.mark.skip(reason="...")          # 无条件跳过
@pytest.mark.skipif(cond, reason="...")  # 条件跳过
@pytest.mark.xfail(reason="...")         # 预期失败
@pytest.mark.parametrize("a,b", [...])   # 参数化
@pytest.mark.slow                        # 自定义标记

同类工具对比

特性Pytestunittestnose2
断言方式简单 assertself.assertEqualassert
Fixture装饰器 + 注入setUp/tearDownsetUp/tearDown
参数化内置需要 subTest内置
插件生态850+ 插件较少中等
学习难度⭐⭐⭐⭐⭐⭐⭐
社区活跃度非常高中等(标准库)低(维护模式)
推荐度⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

总结:Pytest 是 2026 年 Python 测试的事实标准。如果你是新手,直接学 Pytest 就对了,unittest 只需要能看懂老项目代码即可。Pytest 9.0 带来了原生 TOML 配置和核心 subtests 支持,生态更加完善。