内容岛搜索内容
返回上一页

博客园 · 2026年9月10日 16:47

[Python/测试] pytest:简洁、可扩展的 Python 测试框架

0 序 缘起: pytest 做单元测试 1 概述 产品介绍 定位:pytest 是一个面向 Python 的开源测试框架,主打“用最小代价写出可读性高的测试”,同时可扩展到复杂的功能测试、集成测试和端到端测试。 核心理念: 用普通 assert 表达断言,由 pytest 提供失败时的详细信息。

作者:千千寰宇16705 字
0 序
  • 缘起: pytest 做单元测试

image

1 概述

产品介绍

  • 定位:pytest 是一个面向 Python 的开源测试框架,主打“用最小代价写出可读性高的测试”,同时可扩展到复杂的功能测试集成测试端到端测试

  • 核心理念

    • 用普通 assert 表达断言,由 pytest 提供失败时的详细信息
    • fixture(固定支具/固定夹具) 管理测试前置条件和清理逻辑
    • 用 markers、parametrize、plugins 组织和扩展测试
    • 保持与 unittest 生态的兼容,允许渐进式迁移
  • 解决的核心问题

    • 降低测试代码编写成本。
    • 减少传统 unittest 中大量的样板代码。
    • 统一测试发现、参数化、跳过、失败报告和插件扩展机制。
  • URLs

当前项目状态

指标 数据
最新版本 9.1.1
最新版本发布时间 2026-06-19
支持的 Python 版本 Python 3.10+
License MIT
GitHub Stars 14,493
GitHub Forks 3,354
Open Issues/PRs 815
最近一次推送 2026-09-08

数据来源:GitHub API、PyPI API,查询时间为 2026-09-10。

发展历程

  • pytest 的历史较长,官方 README 中标注的项目版权起点为 2004 年现代版本的公开发布记录可以从 1.0 之后清晰追溯。
时间 事件
2004 项目版权起点,早期与 py / py.test 生态相关。
2009-08-04 发布 1.0.0,现代 pytest 版本线开始清晰化。
2010-11-25 发布 2.0.0
2016-08-19 发布 3.0.0;官方文档提到该阶段引入了更清晰的弃用与兼容策略。
2018-11-14 发布 4.0.0
2019-06-29 发布 5.0.0
2020-07-28 发布 6.0.0
2022-02-04 发布 7.0.0
2024-01-27 发布 8.0.0,最低支持 Python 3.8。
2025-11-05 发布 9.0.0,最低支持提升到 Python 3.10。
2026-06-19 发布当前最新版本 9.1.1

长期演进看,pytest 的主线是:

  1. 保持测试写法简单。
  2. 强化 fixture 和插件体系。
  3. 逐步收紧兼容边界。
  4. 跟随 Python 版本演进。
  5. 维持庞大的第三方插件生态。

主要功能

  • 自动测试发现

    • 默认收集 test_*.py*_test.py 文件。
    • 默认收集 test_* 函数和 Test* 类。
  • 增强断言

    • 使用普通 assert
    • 失败时自动输出上下文。
    • 支持 pytest.approx 做近似比较。
  • Fixture 依赖注入

    • 测试通过函数参数声明依赖
    • 支持分层、复用、缓存和 teardown。
  • 参数化测试

    • @pytest.mark.parametrize 可将一组输入展开成多个测试用例。
    • 支持组合参数、pytest.param、id、marks。
  • Markers

    • 支持内置 marker 和自定义 marker。
    • 可用 -m 进行测试选择。
  • 插件体系

    • 内置插件。
    • conftest.py 本地插件。
    • 第三方插件生态。
  • 测试输出控制

    • -v-q-s--tb=short 等。
    • 支持 capsyscaplogtmp_path 等内置 fixture。
  • 跳过与预期失败

    • skipskipifxfail
  • 兼容 unittest

    • 可以直接运行大量已有 unittest.TestCase 测试。
  • 报告与调试

    • --durations 输出耗时。
    • --pdb 在失败处进入调试器。

核心优势

  • 上手成本低
    • 一个普通 Python 函数加一个 assert 就是一个测试。
  • 断言体验好
    • 不需要记忆大量 self.assertEqual 之类的 API。
  • fixture 系统强
    • 相比传统 setup/teardown,fixture 更适合组合、复用和分层。
  • 参数化能力强
    • 一段测试逻辑可以自动展开成多组用例。
  • 生态极大
    • 官方 README 提到外部插件超过 1300 个。
    • 常见方向包括覆盖率、并行执行、Django、asyncio、mock、数据库等。
  • unittest 兼容
    • 老 项目可以渐进迁移,不必一次性重写。
  • 社区成熟
    • 长期维护、版本发布稳定、贡献者众多。
  • 适合 CI/CD
    • 命令行接口稳定,易于接入 GitHub Actions、GitLab CI、Jenkins 等系统。

主要短板

  • 【插件组合】可能带来维护成本
    • 多个第三方插件同时修改收集、报告、fixture 行为时,可能产生兼容性问题。
  • 【隐式行为】较多
    • fixture、autouse、conftest、marker 组合可能降低代码可读性。
  • 复杂测试的【可读性】依赖团队约定
    • 如果滥用高作用域 fixture,测试之间可能出现隐性共享状态。
  • 【并行执行】并非完全内置
    • 官方核心不内置完整并行调度,通常需要 pytest-xdist
  • 对【非常规测试场景】需要插件
    • 例如 Web UI 测试、数据库快照、分布式环境等,通常要结合其他工具或插件。
  • 大型测试套件收集【成本】可能上升
    • 项目很大时,应控制 testpaths、导入路径和插件加载范围。

局限性

  • 仅是【测试框架】,不是【完整的测试管理平台】
    • 不内置测试计划、需求映射、用例评审等产品级功能。
  • 不能解决所有【测试设计】问题
    • fixture 再强,也不能替代良好的测试分层和领域建模。
  • 对 Python 版本有要求
    • 当前 9.x 只支持 Python 3.10+
  • 非 Python 生态支持有限
    • 虽然可以通过插件扩展,但核心优势仍在 Python 项目中。
  • 内部 API 不适合【直接依赖】
    • 第三方插件应尽量使用公开 API 和 hook,避免绑定内部实现细节。

适用场景

  • Python 库或应用的单元测试
  • 服务端 API 集成测试。
  • 数据库访问层测试。
  • CLI 工具测试。
  • 数据处理管道测试。
  • 需要大量参数化场景的算法测试。
  • 需要渐进迁移的 unittest 项目。
  • CI/CD 中的自动化回归测试。
  • 需要利用插件生态扩展覆盖率、并行、mock、日志等能力的项目。

不适合或需要谨慎使用的场景:

  • 测试之间强依赖、共享大量全局状态。
  • 希望只靠框架解决测试质量和测试分层问题。
  • 期望一个工具同时承担测试管理、用例设计和缺陷跟踪。

同类竞品

竞品 定位 与 pytest 的关系
unittest Python 标准库测试框架 更稳定、零依赖,但样板代码更多;pytest 可运行大部分 unittest 测试。
nose2 unittest 扩展 曾经流行,但生态活跃度和插件生态弱于 pytest。
doctest 文档示例测试 适合校验文档示例,不适合作为完整测试框架。
Robot Framework 关键字驱动验收测试 更偏业务流程和 E2E,Python 编程能力不如 pytest 灵活。
Hypothesis 属性测试 可与 pytest 结合,属于互补关系。
tox / nox 多环境管理 不是测试 runner 本身,常与 pytest 搭配使用。
自研测试框架 企业内部定制 官方生态通常更成熟,自研成本高。

发展趋势

  • 社区活跃度高

    • 截至 2026-09-10,最近一次推送为 2026-09-08。
    • 2026 年内已发布多个版本,最新为 9.1.1
  • Star / Fork 规模大

  • 生态继续向工程化扩展

    • 覆盖率、并行、异步、数据库、Web、云环境等方向依赖插件生态快速发展。
  • 兼容策略更清晰

    • 官方维护了较明确的弃用、迁移和兼容政策。
  • 总结

    • pytest 已经从“更好用的 unittest 替代品”发展成 Python 测试生态的事实标准,未来重点会继续放在兼容性、插件生态和大规模工程化上。
2 工作原理与架构

概念术语

术语 含义
test item 一条可执行的测试用例。
collector 负责发现和生成测试项的对象。
fixture 为测试提供前置状态、资源或环境的机制。
scope fixture 的生命周期范围。
marker 附加在测试上的元数据。
parametrize 把一组参数展开为多个测试用例。
conftest.py 目录级配置和本地插件文件。
plugin 扩展 pytest 行为的模块或包。
hook 插件介入 pytest 生命周期的切入点。
assertion rewriting pytest 通过 AST 重写增强普通 assert 的失败信息。

架构与运行原理

pytest 的核心流程可以概括为:

  1. 解析命令行参数和配置文件。
  2. 加载内置插件、第三方插件和 conftest.py
  3. 构建【测试收集树】。
  4. 解析【测试依赖】,包括 fixture、marker、parametrize。
  5. 执行【测试用例】。
  6. 收集断言结果和输出。
  7. 执行 teardown。
  8. 生成报告、缓存和退出码。

结构示意:

flowchart LR A[CLI / config] --> B[Plugin manager] B --> C[Hooks] C --> D[Test collection tree] D --> E[Fixture resolution] E --> F[Run test item] F --> G[Assert / report] F --> H[Teardown] G --> I[Cache / exit code]

运行阶段

阶段 主要动作
启动 解析 pytest.inipyproject.toml、命令行参数。
插件加载 注册内置插件、外部插件和 conftest.py
收集 根据命名规则和配置收集测试。
解析 分析 fixture、marker、parametrize。
执行 调用测试函数或测试类方法。
报告 汇总 pass、fail、skip、xfail、warning。
清理 逆序执行 fixture teardown。
退出 返回 0 到 6 的退出码。

fixture 生命周期

pytest 默认 fixture 作用域是 function。常见作用域包括:

  • function:每个测试函数独立创建和销毁。
  • class:同一个测试类内共享。
  • module:同一个测试模块内共享。
  • package:同一个包内共享。
  • session:整个测试会话内共享。
3 使用指南

安装部署

Linux

python3 -m pip install -U pytest
python3 -m pytest --version

Windows

py -m pip install -U pytest
py -m pytest --version

例如:

(base) PS F:\Codes-Minor\Github\ai-chatbot> .\.venv\Scripts\python -m pytest --version
pytest 8.4.2

如果不使用 py 启动器,也可以使用:

python -m pip install -U pytest
python -m pytest --version

推荐项目结构 (必读)

project/
├── src/
│   └── your_package/
├── tests/
│   ├── conftest.py
│   ├── test_unit.py
│   └── test_integration.py
├── pyproject.toml
└── README.md

image

常见配置

requirements-dev.txt 示例

-r requirements.txt
...
pytest>=8.3.0,<9

image

pyproject.toml 示例

[tool.pytest.ini_options]
minversion = "8.0"
addopts = "-ra --strict-markers"
testpaths = ["tests"]
markers = [
  "slow: marks tests as slow",
  "integration: requires external services",
]

常用配置含义:

  • testpaths:限制默认收集范围,提升速度。
  • addopts:为每次运行追加默认参数。
  • --strict-markers:未注册 marker 直接报错。
  • markers:显式注册自定义 marker,减少拼写错误。

常见用法

基本测试 (必读)

# tests/test_basic.py

def inc(x):
    return x + 1

def test_answer():
    assert inc(3) == 4
  • 运行:
pytest tests/test_basic.py

或:

python -m pytest tests/test_basic.py

或 .\.venv\Scripts\python -m pytest tests/test_basic.py -v
  • -v 参数 : --verbose 参数的缩写,表示 让 pytest 以更详细的方式输出测试结果。
  • 不加 -v 时,输出通常比较简洁:
tests/test_basic.py .                    [100%]
  • -v 后,会显示完整的测试路径和测试名:
tests/test_basic.py::test_answer PASSED  [100%]
tests/test_basic.py::test_eval PASSED    [100%]
tests/test_basic.py::test_login FAILED   [100%]

相关参数: -q/-v/-vv/-s (必读)

参数 含义
-q quiet,简化输出;只看整体结果时用。
-v verbose,显示每个测试的完整 ID 和状态。
-vv 更详细;例如显示更完整的断言差异。
-s 显示测试里的 print() 输出。
  • -s 参数的示例

image

运行指定测试:文件/函数/类/类方法 (必读)

# 按文件/File
pytest tests/test_api.py

# 按函数/Function
pytest tests/test_api.py::test_create_user

# 按类/Class
pytest tests/test_api.py::TestUsers

# 按类方法/Class::Method
pytest tests/test_api.py::TestUsers::test_create_user

按表达式筛选(-k)

image

pytest -k "login and not slow"
   # 注: 运行测试 ID 中包含 login 的测试,但排除名称中包含 slow 的测试。这里的“【名称】”包括:文件名、类名、函数名。

pytest -k "TestUsers or test_create"
pytest -k "not integration"

注意:-k 参数匹配的是“测试名称”里的关键词,不是 marker

  • -k vs -m(marker)
参数 匹配对象
-k 文件名、类名、函数名中的关键词
-m @pytest.mark.xxx 定义的 marker

按 marker 筛选

定义、使用

  • Marker 是 pytest 里的“测试标签/元数据”机制
  • 核心用途:给测试打标记,然后可以按标记筛选、跳过、归类,或让插件识别这些标签并做出特殊处理。
  • 例如:
import pytest

@pytest.mark.slow
def test_login():
    ...

这句话的意思不是“这个测试会自动变慢”,而是:test_login 这个测试用例打上一个名为 slow 的标签。

  • 之后可以在命令行按 marker 筛选:
pytest -m slow          # 只运行带 slow 标记的测试
pytest -m "not slow"    # 运行所有不带 slow 标记的测试
pytest -m "live and not slow"

2种写法

  • 写法1: pytest.mark.slow:标记单个测试
import pytest

@pytest.mark.slow
def test_login():
    ...

含义: test_login 这个测试被标记为 slow
运行:

pytest -m slow

就会选中它。

  • 写法2:pytestmark = pytest.mark.live:标记整个模块
import pytest

pytestmark = pytest.mark.live

def test_login():
    ...

def test_logout():
    ...
  • 含义:当前这个测试模块里的所有测试,都会被标记为 live

等价于给模块里的每个测试都打上 live 标签:

@pytest.mark.live
def test_login():
    ...

@pytest.mark.live
def test_logout():
    ...

live marker = 通常是团队自定义的 marker

image

  • 通常 live 是团队自定义的 marker,常见含义是:
  • 需要真实外部环境
  • 会访问真实网络真实数据库
  • 运行成本高、可能有副作用
  • 【不适合】默认在单元测试中跑。
    例如:
# 默认不跑需要真实环境的测试。
pytest -m "not live"

内置 marker vs 自定义 marker

pytest 自带一些有特殊行为的 marker,例如:

Marker 含义
pytest.mark.skip 无条件跳过
pytest.mark.skipif 条件跳过
pytest.mark.xfail 预期失败
pytest.mark.parametrize 参数化测试
pytest.mark.usefixtures 显式使用 fixture
pytest.mark.filterwarnings 过滤警告
  • liveslowserial 通常是项目自定义标签。它们默认只是元数据,不会自动改变测试行为;需要配合 -m、插件或 hook 使用。

建议在配置里注册,例如:

[tool.pytest.ini_options]
markers = [
    "live: tests that require a live external environment",
    "slow: tests that are slow to run",
]

注册后,pytest 能识别这些 marker,避免“未知 marker”警告。

常用运行参数 (必读)

命令 作用
pytest -v 显示更详细的测试名。
pytest -q 安静模式。
pytest -s 禁用输出捕获,直接显示 print。
pytest -x 首个失败后停止。
pytest --maxfail=3 失败 3 次后停止。
pytest --tb=short 简短 traceback。
pytest --tb=long 详细 traceback。
pytest --durations=10 显示最慢的 10 个测试。
pytest --pdb 失败后进入调试器。
pytest --collect-only -q 只收集不执行。
pytest --fixtures 查看可用 fixture。
pytest --markers 查看可用 marker。

基础 fixture (依赖注入机制) —— 提前准备数据与资源

  • @pytest.fixturepytest依赖注入机制,用来为测试准备它们需要的资源或状态
  • 通俗理解:@pytest.fixture 就是把“测试的前置准备”变成可复用的依赖;测试只要写参数名,pytest 负责准备和注入
  • 核心规则: 测试函数声明什么参数名,pytest 就自动找同名 fixture,执行它并把返回值注入进来。例如:
import pytest

@pytest.fixture
def user():
    return {"id": 1, "name": "Alice"}

def test_user_name(user):
    assert user["name"] == "Alice"

这里的 user 不是普通参数,而是由 fixture 机制提供的测试数据。

  • 常见用途:
  • 构造测试对象
  • 创建数据库/HTTP 客户端
  • 准备临时目录
  • 初始化日志、配置
  • 在测试后清理资源

例如:

@pytest.fixture
def client():
    app = create_app()
    with app.test_client() as c:
        yield c

fixture teardown(测试后清理机制)

  • fixture teardown 是 pytest 的 测试后清理机制,用于在测试完成后释放资源、恢复状态

tear down : 英译: 拆卸、销毁

  • 核心写法:
import pytest

@pytest.fixture
def db():
    conn = connect_db()
    yield conn          # 交给测试使用
    conn.close()        # teardown 机制:测试结束后执行
  • 执行顺序:
  1. 执行 fixture 前半段,创建资源。
  2. yield conn 把资源注入测试。
  3. 测试运行。
  4. 执行 yield 后面的清理代码。
  • 常见清理内容:
  • 关闭数据库连接、文件句柄、HTTP session
  • 删除临时数据
  • 恢复环境变量
  • 停止容器或服务
  • 回滚事务
  • 小结:

@pytest.fixture 负责准备资源yield 之后的代码负责收尾

  • 示例:
import pytest

@pytest.fixture
def service():
    svc = start_service()
    yield svc
    svc.stop() # teardown 机制: `yield` 之后的代码负责【收尾】


def test_health(service):
    assert service.healthcheck() is True

执行顺序:

  1. start_service()
  2. yield svc
  3. 测试执行
  4. svc.stop() —— 测试后的后置动作 (teardown 特性)

fixture 作用域(scope)

import pytest

@pytest.fixture(scope="module")
def client():
    return create_client()

def test_a(client):
    ...

def test_b(client):
    ...
  • 同一个测试模块内,client 只会创建一次

参数化测试

import pytest

@pytest.mark.parametrize(
    ("value", "expected"),
    [
        (1, 1),
        (-2, 2),
        (0, 0),
    ],
)
def test_abs(value, expected):
    assert abs(value) == expected

带 ID:

@pytest.mark.parametrize(
    ("value", "expected"),
    [
        pytest.param(1, 1, id="positive"),
        pytest.param(-2, 2, id="negative"),
    ],
)
def test_abs(value, expected):
    assert abs(value) == expected

临时目录

def test_write_config(tmp_path):
    path = tmp_path / "config.yaml"
    path.write_text("debug: true", encoding="utf-8")

    assert path.read_text(encoding="utf-8") == "debug: true"

会话级临时目录:

import pytest

@pytest.fixture(scope="session")
def data_dir(tmp_path_factory):
    return tmp_path_factory.mktemp("data")

Monkeypatch 环境变量

import os

def test_app_mode(monkeypatch):
    monkeypatch.setenv("APP_MODE", "test")
    assert os.environ["APP_MODE"] == "test"

Mock 属性或函数

from pathlib import Path

def get_ssh_path():
    return Path.home() / ".ssh"

def test_get_ssh_path(monkeypatch):
    monkeypatch.setattr(Path, "home", lambda: Path("/abc"))
    assert get_ssh_path() == Path("/abc/.ssh")

捕获 stdout

def test_stdout(capsys):
    print("hello")
    captured = capsys.readouterr()
    assert captured.out == "hello\n"

近似比较

import pytest

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

conftest.py 共享 fixture

# tests/conftest.py
import pytest

@pytest.fixture
def api_client():
    client = create_test_client()
    yield client
    client.close()

在任意子目录测试中直接使用:

# tests/api/test_users.py
def test_create_user(api_client):
    response = api_client.post("/users", json={"name": "Alice"})
    assert response.status_code == 201

覆盖率测试

通常配合 pytest-cov

python -m pip install pytest-cov
pytest --cov=your_package --cov-report=term-missing

并行执行

通常配合 pytest-xdist

python -m pip install pytest-xdist
pytest -n auto

失败快速停止

pytest -x

失败一批后停止:

pytest --maxfail=5

只查看会执行哪些测试

pytest --collect-only -q

image

推荐日常工作流 (必读)

  1. 先运行最窄范围:
pytest tests/test_that_file.py::test_target_case
  1. 再运行相关模块:
pytest tests/test_that_file.py
  1. 再运行整个相关包:
pytest tests/api
  1. 最后运行全量测试:
pytest
  1. 需要优化速度时,查看耗时:
pytest --durations=10 --durations-min=0.1
Z FAQ for pytest 框架

Q: pytest 能否替代 unittest?

可以替代,但不一定要立刻替换。pytest 可以运行大量已有 unittest.TestCase 测试,因此推荐渐进迁移:

  • 新测试直接用 pytest 写。
  • 老测试先继续运行。
  • 需要重构时再逐步改成 fixture 风格。

Q: pytest 和 unittest 应该选哪个?推荐 pytest

多数 Python 项目更适合选 pytest:

  • 断言更简洁。
  • fixture 更容易组合。
  • 参数化更强。
  • 插件生态更丰富。

unittest 更适合:

  • 希望零第三方依赖。
  • 团队强依赖标准库。
  • 与某些旧工具链绑定较深。

Q: 为什么我的测试没有被收集?*(必读)

常见原因:

  • 文件名称不是 test_*.py*_test.py
  • 函数名不是 test_*
  • 类名不是 Test*,或类主动声明了 __init__
  • 测试文件没有被导入,存在 import error。
  • testpaths 配置没有覆盖目标目录。

排查命令:

pytest --collect-only -q

Q: fixture 和 setup/teardown 的区别是什么?

setup/teardown过程式的生命周期钩子;fixture 是依赖注入式的资源管理机制

fixture 的优势:

  • 可以被多个测试复用。
  • 可以依赖其他 fixture。
  • 可以设置 scope。
  • 可以通过 yield 自动清理。
  • 可以参数化。
  • 可以在 conftest.py 中跨目录共享。

Q: 如何让测试之间互相独立?

建议:

  • 默认使用 function 级 fixture。
  • 避免共享可变对象。
  • 使用 tmp_path 管理临时文件。
  • 使用 monkeypatch 修改属性和环境变量。
  • 数据库测试使用事务回滚或独立数据快照。
  • 避免依赖测试执行顺序。

Q: 如何跳过测试?(必读)

  • 注解方式1
import sys
import pytest

@pytest.mark.skipif(sys.platform == "win32", reason="POSIX only")
def test_posix_feature():
    ...
  • 注解方式2: 无条件跳过:
@pytest.mark.skip(reason="temporary disabled")
def test_not_ready():
    ...
  • 方式3: 运行时条件跳过:
import pytest;

def test_feature():
    if not feature_enabled():
        pytest.skip("feature not enabled")

Q: 如何标记预期失败?(必读)

@pytest.mark.xfail(reason="known bug #123", strict=True)
def test_known_bug():
    ...
  • 参数化中的预期失败:
import pytest

@pytest.mark.parametrize(
    ("value", "expected"),
    [
        (1, 1),
        pytest.param(-1, -1, id="known-bug", marks=pytest.mark.xfail),
    ],
)
def test_normalize(value, expected):
    assert abs(value) == expected

Q: 如何测试异常和警告?

异常

import pytest

def parse_value(value):
    if value is None:
        raise ValueError("bad input")
    return int(value)

def test_bad_input():
    with pytest.raises(ValueError, match="bad input"):
        parse_value(None)

警告

import warnings
import pytest

def test_warning():
    with pytest.warns(UserWarning, match="deprecated"):
        warnings.warn("deprecated API", UserWarning)

Q: 如何并行运行?

核心包不内置完整并行能力,通常使用 pytest-xdist

python -m pip install pytest-xdist
pytest -n auto

前提是测试必须相互独立。对数据库、临时目录、端口等资源要做好隔离。

Q: pytest 的退出码代表什么?(必读)

退出码 含义
0 全部通过
1 有测试失败
2 用户中断
3 pytest 内部错误
4 命令行或配置错误
5 没有收集到测试
6 超过最大警告数

Q: Codex等AI Agent用pytest跑测试用例时,与真人用户用pytest跑测试用例时,默认会出现pytest文件目录的权限冲突(权限不足)问题 (必读)

问题描述

  • 现象1

真人在基于pytest运行测试脚本时,报权限不足的错误。
备注:先用 codex 跑过 pytest 测试用例,后来真人用户又跑过测试用例。

\---
(python-3.13) PS F:\Codes-Minor\Github\ai-chatbot> .\\.venv\Scripts\python -m pytest tests/test\_store.py -v
\=================================================================================== test session starts ====================================================================================
platform win32 -- Python 3.12.8, pytest-8.4.2, pluggy-1.6.0 -- F:\Codes-Minor\Github\ai-chatbot\\.venv\Scripts\python.exe
cachedir: .pytest\_cache
rootdir: F:\Codes-Minor\Github\ai-chatbot
plugins: anyio-4.15.1, langsmith-0.12.4
collected 3 items                                                                                                                                                                          &#x20;

tests/test\_store.py::test\_embedding\_is\_deterministic\_and\_nonzero PASSED                                                                                                               [ 33%]
tests/test\_store.py::test\_persistent\_store\_round\_trip ERROR                                                                                                                           [ 66%]
tests/test\_store.py::test\_history\_and\_knowledge\_search ERROR                                                                                                                          [100%]
...
\================================================================================= short test summary info ==================================================================================
ERROR tests/test\_store.py::test\_persistent\_store\_round\_trip - PermissionError: [WinError 5] 拒绝访问。: 'C:\\\Users\\\Johnny\\\AppData\\\Local\\\Temp\\\pytest-of-Johnny'
ERROR tests/test\_store.py::test\_history\_and\_knowledge\_search - PermissionError: [WinError 5] 拒绝访问。: 'C:\\\Users\\\Johnny\\\AppData\\\Local\\\Temp\\\pytest-of-Johnny'
\========================================================================= 1 passed, 2 warnings, 2 errors in 6.31s ==========================================================================
(python-3.13) PS F:\Codes-Minor\Github\ai-chatbot>

问题原因

  • 现象1: 失败的两个测试使用了 tmp_path:F:\Codes-Minor\Github\ai-chatbot\tests\test_store.py:15F:\Codes-Minor\Github\ai-chatbot\tests\test_store.py:41
# F:\Codes-Minor\Github\ai-chatbot\tests\test_store.py
...

def test_persistent_store_round_trip(tmp_path) -> None: # 测试持久化存储(session / message)的往返操作 | 第15行
    settings = Settings(
        chroma_dir=tmp_path / "chroma",
        session_collection="test_sessions",
        message_collection="test_messages",
        knowledge_collection="test_knowledge",
    )
    store = ChatStore(settings=settings)
    session = store.create_session() //使用了本地存储

    assert store.session_exists(session.session_id)
    assert store.list_sessions() == [session]
    assert store.is_available()

    first = store.add_message(session.session_id, "user", "What is this project?")
    second = store.add_message(session.session_id, "assistant", "A local demo chat bot.")

    assert [record.message_id for record in store.get_messages(session.session_id)] == [
        first.message_id,
        second.message_id,
    ]

    reopened = ChatStore(settings=settings)
    assert reopened.get_messages(session.session_id)[0].content == "What is this project?"

...
  • pytest 默认会使用 %TEMP%\pytest-of-<用户名>,对应代码在 F:\Codes-Minor\Github\ai-chatbot\.venv\Lib\site-packages\_pytest\tmpdir.py:160
# mypy: allow-untyped-defs
"""Support for providing temporary directories to test functions."""

...

class TempPathFactory:
    def __init__(
        self,
        given_basetemp: Path | None,
        retention_count: int,
        retention_policy: RetentionType,
        trace,
        basetemp: Path | None = None, # 第60行 : basetemp
        *,
        _ispytest: bool = False,
    ) -> None:
        check_ispytest(_ispytest)
        ...

    ...
...
  • 经排查,检查到 C:\Users\Johnny\AppData\Local\Temp\pytest-of-Johnny 的 owner 是 LAPTOP-xxx\CodexSandboxOfflineACL 只允许 owner/Admin/SYSTEM 访问;你真人用户的普通 PowerShell 是 Johnny,所以 pytest 读取/清理该目录时报 WinError 5
  • ACL : Windows ACL (Access Control List,访问控制列表)是用于控制对象访问权限和审计的安全机制,包括 DACL 和 SACL 两种类型。Windows安全模型中的核心组件,用于定义用户或组对系统对象(如文件、目录、注册表项等)的访问权限和审计策略。每个ACL由多个ACE(Access Control Entry,访问控制项)组成,每个ACE指定一个用户或组的权限或审计规则。
  • Codex 沙箱进程里仍保留 USERNAME=Johnny

因此,虽然实际运行身份是 CodexSandboxOffline,它也会使用同一个 pytest-of-Johnny,导致2个环境共用并“抢占”这个目录。

解决方法

  • 立即修复:用管理员 PowerShell 执行 Remove-Item -LiteralPath "$env:TEMP\pytest-of-Johnny" -Recurse -Force,然后重新运行测试。
  • 长期避免:普通终端单独设置 Pytest 临时根$env:PYTEST_DEBUG_TEMPROOT = "$env:TEMP\pytest-johnny-terminal",再运行 .\.venv\Scripts\python -m pytest tests/test_store.py -v
  • 或给不同环境使用不同 --basetemp,例如: 普通终端用 .pytest-tmp\user,Codex 沙箱用 .pytest-tmp\codex

  • 补充: powershell 下如何通过命令的方式查看指定目录的创建者账号、权限?

PowerShell 里通常能查到的是 所有者 Owner,Windows 不一定保存“实际创建者”(除非开启了审计/事件记录)

Get-Acl -LiteralPath F:\Codes-Minor\Github\ai-chatbot | Select-Object Path,Owner,Access

或: icacls "F:\Codes-Minor\Github\ai-chatbot"
Y 推荐文献
  • Pytest
X 参考文献

评论 资讯互动

0/500
本站还没有评论

来坐个前排。