Pytest+Tox构建可审计的Python质量流水线

Pytest+Tox构建可审计的Python质量流水线

1. 这不是“又一个测试教程”,而是我踩了三年坑后重写的自动化质量守门员手册

Pytest 和 Tox 这两个词,现在几乎成了 Python 工程师简历里的标配关键词。但说实话,我见过太多团队把它们装进项目里,就像在厨房里摆了一套米其林刀具——看着高级,切个洋葱还划破手指。真正让 Pytest 和 Tox 发挥出“质量守门员”作用的,从来不是pip install pytest tox这一行命令,而是你如何用它们构建一套可验证、可复现、可交接、不依赖个人经验的质量防线。这个标题里说的 “Boost Your Code Quality”,不是靠多写几个断言,而是靠把“谁来测、在哪测、测什么、测完怎么信”这四件事,全部从人脑决策变成配置文件和 CI 流水线里的确定性动作。它解决的核心问题,是当新同事第一天入职、当主分支突然被合并进一段有副作用的代码、当 Python 升级到 3.12 后 CI 突然全红——你不需要开会议、不需要翻聊天记录、不需要靠某位老员工的记忆来救火,只需要看一眼tox -l的输出,再跑一次tox -e py311,就能知道问题出在哪一层。适合谁?适合所有写过超过 500 行 Python 且还在手动python -m pytest tests/的人;适合正在被“本地能过 CI 报错”折磨的团队;更适合那些想把“代码质量”从一句口号,变成一个可度量、可审计、可向产品/老板展示的交付物的技术负责人。这不是教你怎么写assert,而是教你建一条自动质检流水线——原料是你的代码,出口是带签名的、跨环境的、可追溯的质量报告。

2. 为什么非得是 Pytest + Tox?单用一个不行吗?

2.1 Pytest 不是“更好用的 unittest”,它是测试逻辑的“表达式编译器”

很多人第一次接触 Pytest,是从assert直接写条件开始的。这没错,但只看到了冰山一角。Pytest 的核心价值,在于它把“测试用例”从TestCase类的模板束缚中解放出来,变成一种声明式逻辑表达。你可以把它理解成 Python 的“测试 DSL”(领域特定语言)。比如,要测试一个函数在多种输入下的行为,unittest 需要写循环或多个方法:

# unittest 风格:冗长、重复、逻辑分散 class TestCalc(unittest.TestCase): def test_add_positive(self): self.assertEqual(calc.add(2, 3), 5) def test_add_negative(self): self.assertEqual(calc.add(-1, -1), -2) def test_add_mixed(self): self.assertEqual(calc.add(5, -3), 2)

而 Pytest 只需一个函数加参数化装饰器:

# pytest 风格:逻辑集中、意图清晰、扩展成本低 @pytest.mark.parametrize("a,b,expected", [ (2, 3, 5), (-1, -1, -2), (5, -3, 2), ]) def test_add(a, b, expected): assert calc.add(a, b) == expected

这里的关键不是语法糖,而是抽象层级的跃迁@pytest.mark.parametrize不是“让写多个测试更省事”,而是把“测试数据”和“测试逻辑”做了正交分离。数据可以来自 CSV、JSON、数据库查询结果,甚至是一个动态生成的算法;而测试函数本身,只专注“给定输入,校验输出”这一件事。这种分离,直接决定了你后续能否轻松接入模糊测试(fuzz testing)、契约测试(contract testing)或基于模型的测试(model-based testing)。我曾在一个金融风控项目里,把所有规则引擎的测试用例从 Excel 导入,用pytest_generate_tests钩子动态注册,整个测试集从 87 个硬编码用例,扩展到 2300+ 条覆盖边界条件的组合用例,而核心测试函数没动一行。这就是 Pytest 作为“表达式编译器”的威力——它编译的不是字节码,而是你对业务规则的理解。

2.2 Tox 不是“多版本 Python 启动器”,它是环境可信度的“公证处”

如果说 Pytest 解决的是“测什么”和“怎么测”,Tox 解决的就是“在哪测”和“测得准不准”。很多团队尝试过pyenvconda手动切换 Python 版本跑测试,结果往往是:本地py39能过,CI 用py310就挂;或者pip install -r requirements.txt在开发机上成功,但在干净容器里报ModuleNotFoundError。问题根源在于:环境是不可信的。你无法保证“我的开发环境”和“用户安装环境”、“CI 构建环境”、“生产部署环境”在 Python 版本、包版本、系统库、甚至时区设置上完全一致。

Tox 的设计哲学,就是用“隔离”换取“确定性”。它不是简单地调用python3.11 -m pytest,而是为每一次运行,创建一个全新的、空白的、按配置精确初始化的虚拟环境。这个过程包含四个原子步骤:

  1. 环境创建virtualenv --python=python3.11 .tox/py311
    (注意:它不复用你全局的venv,也不污染你的site-packages

  2. 依赖安装pip install pytest flake8 mypackage==0.1.0
    (依赖列表来自tox.inideps,而非你当前目录的requirements.txt

  3. 命令执行.tox/py311/bin/python -m pytest tests/
    (所有命令都在这个纯净环境中执行,PATH、PYTHONPATH 全部重置)

  4. 环境清理(可选)tox --recreate强制重建,确保无缓存污染

这个流程,本质上是在模拟一个“全新用户”从零开始安装并使用你的包的过程。它强制你把所有隐式依赖(比如你以为setuptools是系统自带的,其实新版需要显式声明)都暴露出来。我接手一个遗留项目时,tox -e py38总是失败,查了半小时才发现setup.py里用了pkg_resources,而setuptools没在deps里声明——这在开发机上因为全局安装了setuptools所以能过,但在 Tox 的干净环境里就直接ImportError。Tox 不是给你添麻烦,它是在帮你提前发现那些“只在我机器上能跑”的脆弱假设。

2.3 二者组合:构建“质量契约”的最小可行单元

单独用 Pytest,你得到的是高质量的测试用例;单独用 Tox,你得到的是可靠的执行环境。但只有两者结合,才能形成一份可签署、可审计的“质量契约”。这份契约包含三个关键条款:

  • 条款一:功能正确性(由 Pytest 的assertparametrize保障)
    “当输入 X 时,输出必须是 Y,且满足 Z 约束”。

  • 条款二:环境兼容性(由 Tox 的envlistdeps保障)
    “该功能在 Python 3.8、3.9、3.10、3.11 上均通过,且不依赖任何未声明的第三方包”。

  • 条款三:质量基线(由 Tox 的commands多任务串联保障)
    “每次提交,必须同时通过:单元测试(pytest)、代码风格检查(flake8)、类型检查(mypy)、安全扫描(bandit)”。

这个组合,把“代码质量”从主观感受变成了客观事实。当你在 PR 描述里写上 “tox -e py311passed”,你就不是在说“我觉得没问题”,而是在说“我在一个与生产环境一致的 Python 3.11 环境中,用最新版依赖,完整运行了所有测试和检查,结果为绿”。这背后是工程严谨性的跃迁。我所在团队推行这套流程后,主分支的平均故障恢复时间(MTTR)从 47 分钟下降到 6 分钟——因为 90% 的问题,在开发者本地tox就被拦截了,根本不会推送到远程仓库。

3. 从零搭建:一份可直接抄作业的tox.ini配置详解

3.1 基础骨架:tox.ini的黄金七行

别被网上那些几百行的复杂配置吓到。一个能立刻投入生产的tox.ini,核心就七行。我把它们拆解成“必填项”和“推荐项”,并说明每一行背后的工程考量:

# tox.ini [tox] # 必填:定义你要支持的 Python 环境列表 envlist = py38, py39, py310, py311 [testenv] # 必填:指定每个环境安装哪些依赖(注意:不是 requirements.txt!) deps = pytest>=7.0 pytest-cov>=4.0 # 你的项目源码,以“可编辑模式”安装,这样测试能 import 你的模块 -e . # 必填:定义每个环境要执行的命令 commands = pytest --cov=mypackage --cov-report=term-missing tests/ # 推荐:跳过不存在的 Python 版本(避免在没装 py38 的机器上报错) skip_missing_interpreters = true # 推荐:启用并行测试,加速执行(需 pytest-xdist) # commands = pytest -n auto --cov=mypackage tests/ # 推荐:为不同环境设置不同参数(如旧版本跳过新特性测试) # setenv = # py38: PYTHONPATH = {toxinidir}/src # py311: PYTEST_ADDOPTS = --strict-markers # 推荐:指定 Python 解释器路径(当系统有多个版本时) # basepython = # py38: /usr/bin/python3.8 # py311: /opt/python/3.11/bin/python

提示:-e .是关键中的关键。它表示“以可编辑模式安装当前目录的包”,等价于pip install -e .。这意味着你的测试代码import mypackage时,导入的是你正在修改的源码,而不是 PyPI 上发布的旧版本。没有这一行,你改了代码却总在测旧包,所有测试都失去意义。

3.2 进阶实战:为真实项目定制的多阶段质量流水线

上面是“能跑”,下面是“跑得好、跑得全、跑得懂”。我们以一个典型的 Web API 项目(假设叫fastapi-auth)为例,展示如何用 Tox 编排一个完整的质量流水线。这个配置不再只是跑测试,而是整合了静态检查、动态分析、文档验证和发布前检查:

# tox.ini for fastapi-auth [tox] envlist = lint, typecheck, test, docs, security isolated_build = true # ------------------------------- # 环境 1:代码风格与静态检查 (lint) # ------------------------------- [testenv:lint] deps = black>=23.0 isort>=5.12 flake8>=6.0 pyproject-flake8>=0.4 commands = # 格式化代码(仅检查,不修改) black --check --diff src/ tests/ # 排序 import(仅检查) isort --check --diff src/ tests/ # PEP8 风格检查 flake8 src/ tests/ # ------------------------------- # 环境 2:类型检查 (typecheck) # ------------------------------- [testenv:typecheck] deps = mypy>=1.0 types-requests>=2.0 # 项目自身依赖(用于类型推导) -e . commands = mypy --show-error-codes --pretty src/ tests/ # ------------------------------- # 环境 3:全环境测试 (test) # ------------------------------- [testenv:test] # 支持多个 Python 版本,但测试命令统一 envlist = py39, py310, py311 deps = pytest>=7.0 pytest-asyncio>=0.20 httpx>=0.23 -e . commands = # 并行运行测试,忽略慢测试(开发时用) pytest -n auto -k "not slow" --cov=fastapi_auth --cov-report=html tests/ # 生成覆盖率 HTML 报告,方便查看漏测点 # ------------------------------- # 环境 4:文档构建与链接检查 (docs) # ------------------------------- [testenv:docs] deps = sphinx>=6.0 furo>=2023.0 sphinx-autobuild>=2021.0 commands = # 构建文档 sphinx-build -b html docs/ docs/_build/html # 检查文档内所有链接是否有效(防止死链) sphinx-build -b linkcheck docs/ docs/_build/linkcheck # ------------------------------- # 环境 5:安全扫描 (security) # ------------------------------- [testenv:security] deps = bandit>=1.7 safety>=2.3 commands = # 静态代码安全扫描 bandit -r src/ -x tests/ # 检查已安装依赖是否存在已知 CVE safety check --full-report

这个配置的价值在于:它把原本散落在Makefilepre-commitCI 脚本里的各种检查,全部收口到一个统一的入口tox。开发者只需记住:

  • tox -e lint:提交前快速检查代码风格
  • tox -e typecheck:确认类型注解无误
  • tox -e test:运行所有测试(可指定tox -e test-py311
  • tox -e docs:本地预览文档效果
  • tox -e security:发布前做最终安全审计

注意:isolated_build = true是现代 Python 项目的必备项。它强制 Tox 使用pyproject.toml中定义的构建后端(如setuptoolshatchling)来构建你的包,而不是依赖你本地的setup.py。这保证了构建过程与 PyPI 官方构建完全一致,避免了“本地能打包,PyPI 构建失败”的经典陷阱。

3.3 参数计算:如何科学地选择envlist?别盲目堆版本

看到别人envlist = py37, py38, py39, py310, py311, py312,就照抄?这是最大的误区。envlist不是版本越多越好,而是要基于你的用户真实环境分布Python 官方支持周期做科学取舍。我用一个简单的决策树来说明:

  1. 第一步:查官方支持状态
    访问 https://devguide.python.org/versions/ (Python 官方开发指南),确认各版本状态:

    • py37:已于 2023-06-27EOL(End of Life),官方停止维护。
    • py38:将于 2024-10-01 EOL。
    • py39:将于 2025-10-01 EOL。
    • py310+:均为活跃支持版本。
  2. 第二步:查你的用户数据
    如果你有生产监控,看过去 30 天用户使用的 Python 版本分布(可通过sys.version上报);如果没有,查你的主要依赖库(如fastapi,sqlalchemy)的setup.pypyproject.toml,看它们声明的python_requires。例如,fastapi>=0.100.0要求python>=3.8,那么py37就毫无意义。

  3. 第三步:做减法,保留“关键节点”
    基于以上,一个务实的envlist应该是:
    envlist = py39, py310, py311
    理由

    • py39:覆盖大量仍在使用 Ubuntu 20.04 / CentOS 8 的企业用户(这些系统默认 Python 3.8/3.9)。
    • py310:Python 3.10 是第一个全面支持结构化模式匹配(match/case)的稳定版,是新项目主流起点。
    • py311:最新稳定版,性能提升显著(CPython 3.11 比 3.10 快 10-25%),且是未来 2 年的主力。
      砍掉py37py38:它们已 EOL 或即将 EOL,继续测试是浪费 CI 资源;不加py312:除非你明确要支持,否则等它成为python_requires的最低要求时再加入(通常滞后 3-6 个月)。

这个决策过程,比盲目堆砌版本更能体现工程专业性。我曾帮一个客户将 CI 测试矩阵从 6 个环境缩减到 3 个,CI 总耗时从 22 分钟降到 9 分钟,而线上 Python 版本相关 Bug 零增长——因为测试资源被精准投向了真正的风险地带。

4. 实操避坑:那些官网不会告诉你的 Tox 和 Pytest 秘密武器

4.1 Pytest 的conftest.py:不是“配置文件”,而是测试世界的“中央处理器”

几乎所有 Pytest 教程都会告诉你conftest.py是“存放 fixture 的地方”。这太浅了。conftest.py的真实身份,是整个测试包的共享上下文中心。它能在测试执行前、中、后,注入任意逻辑。我用三个真实场景说明它的威力:

场景一:自动注入测试数据库连接(避免每个 test 文件都写 setup/teardown)
在项目根目录的tests/conftest.py里:

import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from mypackage.database import Base @pytest.fixture(scope="session") def db_engine(): """为整个测试会话创建一个共享的 SQLite 内存数据库引擎""" engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) # 创建所有表 return engine @pytest.fixture(scope="function") def db_session(db_engine): """为每个测试函数创建独立的事务会话,自动 rollback""" connection = db_engine.connect() transaction = connection.begin() Session = sessionmaker(bind=connection) session = Session() yield session session.close() transaction.rollback() connection.close()

现在,任何tests/下的测试函数,只要声明def test_something(db_session):,就能获得一个干净、隔离、自动回滚的数据库会话。你不用管连接怎么建、事务怎么开、怎么关——conftest.py全包了。

场景二:动态跳过不兼容的测试(解决py38vspy311语法差异)

import sys import pytest # 在 conftest.py 中 def pytest_runtest_makereport(item, call): """钩子:当测试因语法错误(SyntaxError)失败时,自动标记为 skip""" if call.excinfo is not None and call.excinfo.typename == "SyntaxError": # 检查是否是新语法(如 py311 的 'except*') if sys.version_info < (3, 11) and "except*" in str(call.excinfo.value): # 对于旧版本,跳过这个测试,而不是让它 fail pytest.skip("Requires Python 3.11+ for except* syntax") # 或者更直接:用 mark def pytest_configure(config): config.addinivalue_line( "markers", "requires_py311: marks tests as requiring Python 3.11+" ) def pytest_collection_modifyitems(config, items): if sys.version_info < (3, 11): skip_311 = pytest.mark.skip(reason="Requires Python 3.11+") for item in items: if "requires_py311" in item.keywords: item.add_marker(skip_311)

然后在测试里:

@pytest.mark.requires_py311 def test_exception_group(): try: raise ExceptionGroup("eg", [ValueError("v1"), TypeError("t2")]) except* ValueError: pass

场景三:自定义测试报告(把失败的 SQL 查询日志打出来)

# conftest.py def pytest_runtest_makereport(item, call): if call.when == "call" and call.excinfo is not None: # 如果是数据库测试失败,尝试打印最后执行的 SQL if hasattr(item, "funcargs") and "db_session" in item.funcargs: from sqlalchemy import text # 获取 session 的 last executed statement(需适配你的 ORM) # ... 逻辑略 ... # report.longrepr = f"{report.longrepr}\n\nLast SQL: {last_sql}" return report

实操心得:conftest.py的作用域是“它所在目录及其所有子目录”。所以tests/conftest.py对所有tests/下的测试生效;tests/integration/conftest.py只对tests/integration/下的测试生效。合理利用这个层级,可以构建出非常精细的测试上下文。

4.2 Tox 的recreate--force-reinstall:不是“重启大法”,而是环境可信度的终极校验

tox -r(即--recreate)是 Tox 最常被滥用的命令。很多人一遇到问题就tox -r,以为是“清缓存”。其实,--recreate的真正含义是:销毁当前环境,并从头开始执行“环境创建 -> 依赖安装 -> 命令执行”的完整流程。它解决的不是缓存问题,而是环境漂移(environment drift)

什么是环境漂移?举个例子:

  • 第一次tox -e py310,它创建环境,安装pytest==7.2.0
  • 你手动进入.tox/py310,用pip install pytest==7.3.0升级了 pytest。
  • 下次tox -e py310,它不会重新安装 pytest,而是直接复用这个被你“污染”的环境,导致测试行为与预期不符。

tox -r就是把这个被污染的环境彻底删除,重建一个“出厂设置”的纯净环境。这才是它存在的意义。

--force-reinstall更进一步:它强制重新安装所有deps列表里的包,即使版本号没变。这在以下场景至关重要:

  • 场景:你修改了pyproject.toml中的dependencies,但 Tox 没检测到变化
    Tox 默认只检查tox.inisetup.py的修改时间戳。如果你用的是pyproject.toml构建,且deps里引用了.[test]这样的额外依赖,Tox 可能不会感知到pyproject.toml的变更。此时tox -e test --force-reinstall能确保所有依赖按最新pyproject.toml重新解析安装。

  • 场景:你怀疑某个包的 wheel 缓存损坏
    pip有时会缓存损坏的 wheel 文件。--force-reinstall会绕过缓存,强制从 PyPI 重新下载安装。

注意:--force-reinstall不会重新创建虚拟环境,只重装包。所以它比-r快得多,是日常调试的首选。我自己的工作流是:先tox -e test --force-reinstall,如果还不行,再tox -r -e test

4.3 CI 集成:GitHub Actions 中 Tox 的最佳实践(非 YAML 模板,而是原理)

很多团队把 Tox 配置好后,就直接扔进 GitHub Actions 的run: tox里。结果 CI 经常失败,报错No module named 'mypackage'。问题出在 CI 环境的“工作目录”和 Tox 的“包安装逻辑”上。真相是:Tox 默认不会自动安装你的项目包,除非你明确告诉它

在 GitHub Actions 中,标准的checkout步骤后,工作目录是你的仓库根目录。此时,如果你的tox.ini里有-e .,Tox 会执行pip install -e .。但这要求你的项目必须有合法的pyproject.tomlsetup.py。而很多新手项目,pyproject.toml是空的,或者setup.pypackages=find_packages()没配对。

正确的做法,是把 Tox 的安装逻辑显式化,并与 CI 的缓存策略对齐:

# .github/workflows/test.yml name: Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v4 # 关键一步:预安装 tox,避免每次都要 pip install - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install tox run: pip install tox # 关键一步:使用 tox 的 --sitepackages 选项(谨慎!) # 这会让 tox 环境继承系统 Python 的 site-packages # 适用于:你的项目是纯脚本,没有 setup.py,且依赖都是基础库 # - name: Run tests # run: tox -e py${{ matrix.python-version }} --sitepackages # 推荐做法:确保你的项目有合法的 pyproject.toml # 并在 tox.ini 的 deps 中显式声明 -e . # 这样,tox 会自动在每个环境中 pip install -e . - name: Run tests run: tox -e py${{ matrix.python-version }} # 关键一步:缓存 tox 环境,加速后续运行 # tox 会把环境存在 .tox/ 目录下,缓存它即可 - name: Cache tox environments uses: actions/cache@v3 with: path: .tox key: ${{ runner.os }}-tox-${{ hashFiles('**/tox.ini') }}

实操心得:CI 中最耗时的不是测试本身,而是环境创建和依赖安装。.tox目录缓存能将单次tox -e py311的耗时从 90 秒降到 12 秒。但要注意,key必须包含tox.ini的哈希值,否则配置一改,缓存就失效,CI 还是慢。另外,永远不要在 CI 中用tox --recreate,这会杀死缓存,让每次构建都从零开始。

5. 常见问题速查表:从报错信息直达解决方案

报错信息(截取关键部分)根本原因一招解决为什么这招有效
ERROR: invocation failed (exit code 1), logfile: .../log/py311-1.log
Command 'python -m pytest ...' returned non-zero exit code 1
Pytest 本身执行失败(如测试断言失败、代码异常),不是 Tox 错误cat .tox/py311/log/py311-1.log查看完整 pytest 输出Tox 的 log 目录结构是.tox/<env>/log/<env>-<number>.log,里面是 pytest 的原始 stdout/stderr,比 Tox 的 summary 更详细
ERROR: unknown environment 'py312'本地没有安装 Python 3.12 解释器,或 Tox 找不到它pyenv install 3.12.0 && pyenv global 3.12.0(macOS/Linux)
choco install python312(Windows)
Tox 的py312环境名,是它去系统 PATH 里找python3.12python命令。没装,自然找不到。pyenv是最可控的管理方式。
ERROR: Could not find a version that satisfies the requirement mypackage==0.1.0tox.inideps里写了mypackage==0.1.0,但 PyPI 上没有这个版本deps中的mypackage==0.1.0改成-e .-e .表示安装当前目录的包,不走 PyPI。这是开发阶段的唯一正确写法。发布后才用mypackage>=0.1.0
ImportError: No module named 'mypackage'Tox 环境里没安装你的包,或pyproject.toml配置错误1. 确认tox.inideps包含-e .
2. 确认pyproject.toml[build-system][project]
Tox 不会自动识别你的包。-e .是显式指令;而pyproject.toml是告诉pip如何构建它。缺一不可。
ERROR: invocation failed (exit code 2), logfile: .../log/lint-1.log
black --check failed
black格式化检查失败,代码不符合规范black src/ tests/(在本地运行,它会自动格式化)black --check只检查,不修改。black命令本身会修改代码。CI 失败,说明代码格式不合规,本地运行black修复即可。
ERROR: Package 'setuptools' requires a different Python: 3.11.0 not in '>=3.7, <3.11'你的pyproject.tomlrequires-python写死了<3.11,但你在py311环境运行修改pyproject.tomlrequires-python = ">=3.7, <3.12"requires-python是给pip看的,告诉它“这个包只能在哪些 Python 版本上安装”。Tox 的py311环境会严格遵守它。写得太窄,就会冲突。
WARNING: Discarding ... (py311) because it's not compatible with this PythonTox 尝试安装一个 wheel,但 wheel 的python_versiontag 不匹配tox.ini[testenv]下添加ignore_basepython_conflict=true某些包(尤其是 C 扩展)的 wheel 只编译了特定 Python 版本。ignore_basepython_conflict会让 Tox 忽略这个警告,改用源码安装(sdist),虽然慢一点,但能过。

实操心得:Tox 的报错信息,90% 都指向“环境配置”或“依赖声明”,而不是你的业务代码。所以看到报错,第一反应不应该是打开你的test_xxx.py,而是打开tox.inipyproject.toml,检查envlistdepsrequires-python这三要素是否自洽。我有个习惯:每次 Tox 报错,先tox -e py311 --print-deps,它会打印出 Tox 计划安装的所有依赖及其版本,一眼就能看出哪个包冲突了。

6. 超越基础:用 Pytest + Tox 构建可审计的质量交付物

6.1 生成一份“质量护照”:把测试报告变成可交付的 PDF

很多团队的测试报告,只存在于 CI 的控制台日志里,无法归档、无法分享、无法审计。Pytest 和 Tox 可以帮你生成一份真正的“质量护照”——一份包含所有质量维度的、格式化的、可签名的 PDF 报告。

核心工具链:pytest-html+weasyprint+tox

第一步:安装报告生成器

# 在 tox.ini 的 deps 里加上 [testenv:report] deps = pytest-html>=4.0 weasyprint>=60.0 -e .

第二步:编写生成报告的命令

[testenv:report] commands = # 1. 运行所有测试,并生成 HTML 报告 pytest --html=reports/test-report.html --self-contained-html tests/ # 2. 生成类型检查报告 mypy --show-error-codes --pretty src/ > reports/type-report.txt 2>&1 # 3. 生成安全扫描报告 bandit -r src/ -o reports/bandit-report.json -f json # 4. (可选)用 weasyprint 把 HTML 转 PDF # weasyprint reports/test-report.html reports/test-report.pdf

第三步:在 CI 中自动归档

# GitHub Actions - name: Upload quality report uses: actions/upload-artifact@v3 with: name: quality-passport path: reports/

现在,每次 CI 成功,你都会得到一个reports/目录,里面包含:

  • test-report.html:交互式测试报告,可点击展开失败详情
  • type-report.txt:完整的 mypy 类型错误列表
  • bandit-report.json:结构化的