tox + testr 单元测试工作流程详解:从环境隔离到并行执行
两个工具各做什么
工具 全称 核心职责
tox tox 标准化的虚拟环境管理 + 测试编排
testr testrepository 测试仓库管理、并行执行、结果持久化
一句话概括:
tox 解决的是「在干净的虚拟环境里跑测试,避免依赖污染」。
testr 解决的是「几十万测试怎么并行跑、跑完的结果如何归档复用」。
tox
↓ 创建/复用虚拟环境
↓ 执行 commands
python setup.py testr --slowest --coverage --testr-args=“–subunit”
↓ 调用 pbr 的 testr 入口
testr run
↓ 读取 .testr.conf
↓ 根据 test_command 启动测试进程
python -m subunit.run discover -t ./ ./tests/unit
↓ 执行测试用例,输出 subunit v1 流
↓ testr 捕获流
↓ 写入 .testrepository/{next_number}
↓ 合并历史结果
testr last --subunit
↓ 输出最新一次运行的 subunit 流
subunit-trace --no-failure-debug -f
↓ 转换为人类可读的实时输出
两者解耦 —— tox 可以单独跑 pytest/nose,testr 也可以独立于 tox 使用。组合起来就形成「环境隔离 + 高性能并行 + 结果仓库」的完整闭环。
二、tox 工作原理
2.1 核心机制
tox 读取 tox.ini(或 pyproject.toml 中的 [tool.tox]),按其中定义的 testenv 创建虚拟环境、安装依赖、执行命令。
tox.ini
├── [tox] 全局配置
├── [testenv] 默认环境模板
├── [testenv:py311] 具名环境:Python 3.11
├── [testenv:pep8] 具名环境:代码风格检查
└── [testenv:cover] 具名环境:覆盖率
2.2 执行步骤
- 解析 tox.ini,确定要运行的 env(默认 [testenv],或 -e 指定)
- 在 .tox// 下创建 venv
- 安装 deps = … 列出的依赖
- 安装项目本身(pip install .)
- 执行 commands = … 中的命令
- 收集退出码,汇总结果
2.3 典型 tox.ini
[tox]
envlist = py39,py311,pep8,cover
skipsdist = False
[testenv]
usedevelop = True
setenv =
VIRTUAL_ENV={envdir}
LANG=en_US.UTF-8
deps =
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
python -m testr run {posargs}
[testenv:pep8]
deps = flake8
commands = flake8 {toxinidir}/myproject
[testenv:cover]
setenv =
{[testenv]setenv}
PYTHON=coverage run --source myproject -m testr run
commands =
coverage erase
{env:PYTHON}
coverage report
coverage html
注意 commands = python -m testr run —— 这就是 tox 把测试执行权交给 testr 的关键。
[tox]
envlist = py27,cover
skipsdist = False
[testenv]
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
testrepository
subunit
[testenv:cover]
setenv = VIRTUAL_ENV={envdir}
NOSE_WITH_COVERAGE=1
commands =
python setup.py testr --slowest --coverage
–testr-args=“–subunit”
tox -ecover --sitepackages -v 各参数含义:
参数 含义
-e cover 选择执行 cover 环境
–sitepackages 复用系统已安装的包,不重新虚拟化所有依赖
-v 详细输出
三、testr 工作原理
3.1 核心概念:testrepository
testr 在项目根目录维护一个 .testr/ 目录,里面存放:
.testr/
├── config # testr 配置
├── last # 上次运行的 subunit 流
├── slowest # 最慢测试列表
├── failing # 最近失败的测试
└── history # 历史运行记录
这是一个持久化的测试仓库,不只是一次性的执行框架。
3.2 subunit 协议
testr 的核心数据格式是 subunit —— 一种二进制/文本混合的测试结果流协议,兼容 xUnit 但更丰富。
test: mypackage.tests.test_foo.TestFoo.test_bar
success: mypackage.tests.test_foo.TestFoo.test_bar (1.234s)
特点:
流式传输,边跑边出结果
支持多进程汇流(并行测试的多个 worker 写入同一流)
兼容 Python unittest、pytest、nose 等主流框架
3.3 .testr.conf
[DEFAULT]
test_command = python -m subunit.run discover -t . $TESTS_DIR
test_list_command = python -m subunit.run discover -t . $TESTS_DIR --list
group_regex = ([^.]+.)
test_run_concurrency = 1
test_command:实际执行测试的命令,subunit.run 是 subrun 的入口,负责输出 subunit 流
test_list_command:列出所有可用测试 ID(用于过滤、并行分片)
group_regex:把测试按前缀分组,用于并行调度
test_run_concurrency:并行度
3.4 核心参数详解
python setup.py testr --slowest --coverage --testr-args=“–subunit”
参数 含义
–slowest 测试结束后显示最慢的 10 个用例
–coverage 自动启用 coverage(生成 .coverage 文件)
–testr-args=“–subunit” 透传给 testr run,让 testr 输出 subunit 流
–coverage 的工作机制:
testr 在执行前向 test_command 注入 coverage 包装
等价于 python -m coverage run --source= -m subunit.run discover …
每个测试进程会写自己的 .coverage.{pid} 文件
后续脚本 coverage combine 合并所有进程的覆盖率数据
四、完整工作流程
把 tox 和 testr 串起来,一次 tox -e py311 的内部流程:
┌─────────────────────────────────────────────────────────────┐
│ Step 1: tox 解析 tox.ini,确定运行 env=py311 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 2: tox 在 .tox/py311/ 创建虚拟环境 │
│ (若已存在且无 skip-install,直接复用) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 3: pip install -r requirements.txt │
│ pip install -r test-requirements.txt │
│ pip install -e . (项目本身,usedevelop=True) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 4: 执行 commands: python -m testr run │
│ 此刻控制权从 tox 转交给 testr │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 5: testr 读取 .testr/config 获取 test_command │
│ 读取 .testr/history 准备 failing-first 调度 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 6: testr 调用 test_list_command 列出全部测试 ID │
│ 按 group_regex 切分,分发给 N 个并行 worker │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 7: 每个 worker 执行: │
│ python -m subunit.run discover -t . $TESTS_DIR <test_ids> │
│ 输出 subunit 流到 stdout │
└─────────────────────────────────────────────────────┬───────┘
│
┌───────────────────────────────────────────────┘
│ (多个 worker 的 subunit 流汇入)
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 8: testr 把 subunit 流 tee 到: │
│ - .testr/last (上次完整流) │
│ - .testr/history (累积历史,供下次调度) │
│ - 实时渲染到终端 (subunit2py 或 subunit2html) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 9: testr 统计 pass/fail/skip,以 exit code 返回 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 10: tox 接收到 exit code, │
│ 非 0 则标记 env 失败,汇总到最终结果 │
└─────────────────────────────────────────────────────────────┘
