OpenStack 单元测试工作流程详解:tox + testr

tox + testr 单元测试工作流程详解:从环境隔离到并行执行

在 OpenStack 等大型 Python 项目中,toxtestr 是一对经典组合:tox 负责环境隔离 ,testr 负责测试仓库 +
并行执行 + 历史结果管理
。本文系统讲解两者协作的完整工作流程。


一、两个工具各做什么

工具 全称 核心职责
tox tox 标准化的虚拟环境管理 + 测试编排
testr testrepository 测试仓库管理、并行执行、结果持久化

一句话概括:

  • tox 解决的是「在干净的虚拟环境里跑测试,避免依赖污染」。
  • testr 解决的是「几十万测试怎么并行跑、跑完的结果如何归档复用」。
text 复制代码
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 执行步骤

复制代码
1. 解析 tox.ini,确定要运行的 env(默认 [testenv],或 -e 指定)
2. 在 .tox/<envname>/ 下创建 venv
3. 安装 deps = ... 列出的依赖
4. 安装项目本身(pip install .)
5. 执行 commands = ... 中的命令
6. 收集退出码,汇总结果

2.3 典型 tox.ini

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 的关键。

ini 复制代码
[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 unittestpytestnose 等主流框架

3.3 .testr.conf

ini 复制代码
[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 失败,汇总到最终结果                   │
└─────────────────────────────────────────────────────────────┘

五、testr 常用命令

bash 复制代码
# 初始化测试仓库(项目根目录)
testr init

# 运行全部测试
testr run

# 运行指定测试(支持正则)
testr run mypackage.tests.test_foo

# 只跑上次失败的测试(failing-first,加速回归)
testr run --failing

# 查看上次运行结果
testr last

# 查看 N 次运行中最慢的测试
testr slowest

# 列出上次失败的测试
testr failing

# 在上次失败的基础上再次跑
testr run --subunit | subunit-filter --no-success | subunit2pyunit

# 把上次的 subunit 流转成 HTML 报告
testr last --subunit | subunit2html output.html

六、并行执行机制

testr 的并行能力是其最大价值之一。流程:

复制代码
1. testr 调用 test_list_command 获取全部 test ID
2. 按 group_regex 把测试分成若干「组」(同组测试不会拆到不同 worker,避免共享 fixture 冲突)
3. 启动 N 个 worker 进程(默认 = CPU 核数)
4. 每个 worker 拿到自己的测试子集,通过 test_command 执行
5. 各 worker 的 subunit 流汇入 testr 的总流
6. testr 实时把汇总流写入 .testr/last 和终端

配置并发数:

bash 复制代码
# 通过环境变量
TESTRUN_CONCURRENCY=8 testr run

# 或在 .testr.conf 中指定
[DEFAULT]
test_concurrency=8

注意 :并行要求测试之间无共享状态/全局 fixture。如果用 setUpClass/setUpModule 做数据库初始化,多 worker

会各自跑一次,可能造成连接数爆炸。


七、subunit 流的处理

subunit 是 testr 体系的「血液」。常用管道命令:

bash 复制代码
# 把上次结果中失败的测试抽出来再跑
testr last --subunit | subunit-filter --no-success | subunit-run

# 实时把 subunit 流转换成人类可读输出
testr run --subunit | subunit2pyunit

# 转 HTML
testr run --subunit | subunit2html report.html

# 转 JUnit XML(兼容 CI)
testr run --subunit | subunit2junitxml junit.xml

# 过滤出只跑某个 package
testr run --subunit | subunit-filter --name-pattern 'mypackage.tests.test_db' | subunit-run

这是 testr 区别于 pytest 的关键:测试结果是一段可流动的数据,不是固定的报告文件。你可以任意接管道处理。


八、最小可运行示例

8.1 项目结构

复制代码
myproject/
├── myproject/
│   ├── __init__.py
│   └── foo.py
├── myproject/tests/
│   ├── __init__.py
│   ├── test_foo.py
├── tox.ini
├── .testr.conf
├── requirements.txt
└── test-requirements.txt

8.2 test-requirements.txt

复制代码
testrepository
subunit
fixtures
testtools

8.3 .testr.conf

ini 复制代码
[DEFAULT]
test_command = python -m subunit.run discover -t . myproject/tests $LISTOPT
test_list_command = python -m subunit.run discover -t . myproject/tests --list

8.4 tox.ini

ini 复制代码
│ 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 失败,汇总到最终结果                   │
└─────────────────────────────────────────────────────────────┘

九、failing-first:核心加速技巧

testr 的 .testr/history 记录了每个测试最近的通过/失败状态。下次跑时:

复制代码
testr run --failing

优先调度上次失败的测试,先跑、先报错。如果开发者在改动的就是那部分代码,几秒就能看到红;如果之前的失败已经修好,剩余测试再继续跑。

这相当于把「全量回归」的反馈时间从「全部跑完才看到失败」变成「失败先暴露」,在大型项目里收益巨大。


十、常见坑

坑 1:testr init 未执行

第一次跑 testr run 会报 No testrepository found

bash 复制代码
# 解决
testr init

tox.ini 里可以加一行自动初始化:

ini 复制代码
commands =
    testr init
    testr run {posargs}

坑 2:并行下 fixture 冲突

例如 setUpClass 里建表,多个 worker 同时建,数据库报错。

解决:

  • 把不可并行的 fixture 改成模块级单例 + 锁
  • 或降低 test_concurrency=1 退回串行
  • 或用 group_regex 把相关测试归到同一 worker

坑 3:subunit.run 找不到测试

subunit.run discover 默认从当前目录发现,如果 TESTS_DIR 没设对,会跑空。

ini 复制代码
[DEFAULT]
test_command = python -m subunit.run discover -t . myproject/tests $LISTOPT
#                                            ^^^^^^^^^^^^^^^^^ 明确指定目录

坑 4:覆盖率和 testr 一起跑

不能直接 coverage run testr run,因为 testr 会 fork 子进程,coverage 抓不到。

正确做法:

ini 复制代码
[testenv:cover]
setenv = PYTHON=coverage run --source myproject -m testr run
commands =
    coverage erase
    {env:PYTHON}
    coverage report

把 coverage 作为 python 解释器前缀 注入,让每个 worker 都在 coverage 下跑。

坑 5:Windows 兼容性

testr 在 Windows 上偶发问题(并行进程、路径分隔符)。如果必须在 Windows 跑,建议:

ini 复制代码
[testenv]
setenv = TESTRUN_CONCURRENCY=1

退回串行,稳定性优先。


十一、和 pytest 的对比

维度 tox + testr pytest + pytest-xdist
环境隔离 tox 原生支持 需配合 tox 或 nox
并行 testr 调度,基于 subunit pytest-xdist,worker 进程
结果持久化 .testr/history,可 failing-first 需 pytest-cache 或 --lf
流式协议 subunit,可流式管道处理 原生流式但不可管道组合
生态 OpenStack 系,偏传统 现代主流
配置复杂度 高(tox.ini + .testr.conf) 低(pyproject.toml 一处)
学习曲线

建议:

  • 老项目、OpenStack 系、需要 subunit 流生态 → 继续 tox + testr
  • 新项目、Python 3.10+、追求简洁 → tox + pytestnox + pytest

十二、最佳实践清单

  1. .testr/ 加入 .gitignore ,但 .testr.conf 提交
  2. CI 中先 testr inittox,避免空仓库报错
  3. 本地用 --failing 做快速回归,CI 用全量跑
  4. group_regex 一定要设,否则并行下共享 fixture 测试会炸
  5. 覆盖率和并行分开跑,coverage 走单独 env
  6. subunit2html 产物归档,方便回溯历史失败
  7. 不要在 test_command 里写死 worker 数,用环境变量注入,方便 CI 调
  8. 定期清理 .testr/history,长期累积会变慢
  9. CI 用 testr last --subunit | subunit2junitxml 生成 JUnit 报告,Jenkins/GitLab 原生解析
  10. 大型项目把单元/集成测拆成不同 env,testr 只管单测,集成测用别的工具

十三、速查表

复制代码
# 项目初始化
testr init

# 全量跑
tox -e py311

# 只跑失败
tox -e py311 -- --failing

# 跑指定测试
tox -e py311 -- mypackage.tests.test_foo.TestBar.test_baz

# 看上次结果
testr last

# 看最慢的 20 个
testr slowest

# 转 HTML
testr last --subunit | subunit2html report.html

# 转 JUnit XML
testr last --subunit | subunit2junitxml junit.xml

# 重置仓库
rm -rf .testr && testr init

十四、结语

tox + testr 的组合,核心是三层解耦:

工具 优势
tox 隔离测试环境,避免污染系统 Python;--sitepackages 复用预装包加速
testr 支持测试并行(按 group_regex 分组),自动重跑失败用例,subunit 流归档可复现
subunit 与语言无关的标准测试流格式,能被多种工具(trace/junitxml/html)二次加工

这套组合是 OpenStack 生态的标准实践,脚本能复用现成的 subunit2junitxml、subunit2html、xunitmerge 工具链。

理解了这三层,后续无论是接入 CI、做覆盖率、生成 HTML 报告,都是「在管道上接插件」的事。testr 在新项目里被 pytest

取代是大势所趋,但读懂它的设计,对理解大型 Python 项目的测试架构仍有价值。


相关推荐
i2英方软件4 小时前
OpenNebula 与 OpenStack:您应该选择哪一个?
openstack·opennebula·开源私有云对比
胖兔纸25 天前
OpenStack 1.7.2 & Ceph 9.2.1 运维命令
运维·ceph·openstack
24计网1王仔寿12 天前
Linux 系统运维全栈学习路线|从 Shell 脚本到容器云 OpenStack 完整学习指南
linux·学习·openstack
m0_547486661 个月前
《KVM Docker OpenStack实战—虚拟化与云计算配置 管理与运维》全套课件PPT
运维·docker·openstack
猫头虎-前端技术2 个月前
JS 作用域与闭包:从变量提升到闭包陷阱的超详细解析
开发语言·javascript·云计算·bootstrap·ecmascript·openstack·perl
笑脸笑脸2 个月前
OpenStack管理-nova计算
openstack
潮起鲸落入海2 个月前
OpenStack块存储管理-cinder对象存储-swift
openstack·swift
米高梅狮子2 个月前
01.CentOS-Stream-8-packstack安装OpenStack
linux·云原生·容器·kubernetes·centos·自动化·openstack