Moby 测试体系完全指南:测试套件划分、编写规范与 make 测试命令实战
Moby(Docker 引擎的开源仓库)为贡献者提供了一套清晰分层的测试体系:快速的单元测试(unit tests)、面向 HTTP API 的集成测试(integration/),以及已经弃用的遗留套件(integration-cli/)。本文基于仓库根目录的 TESTING.md 展开,结合 Makefile、hack/test/unit、hack/make/test-integration 等脚本源码,系统讲解 Moby 的测试分层原则、新测试编写规范、集成测试环境判断技巧,以及如何用 make 目标和环境变量精确控制测试的运行范围,帮助你在修改 Moby 代码后能够正确选择并运行对应的测试套件。
一、Moby 的测试套件分层
Moby 将测试划分为两套正式套件加一套遗留套件:
| 套件 | 位置 | 工具与断言 | 定位 |
|---|---|---|---|
| 单元测试(Unit tests) | 与源码同包 | go test + gotest.tools/v3/assert |
要求快速,只测试自身所在包 |
| API 集成测试 | ./integration/<component>(container、image、volume 等) |
go test + gotest.tools/v3/assert |
向 API 端点发起真实 HTTP 请求,校验 HTTP 响应与调用后的 daemon 状态 |
| 遗留集成测试(已弃用) | integration-cli/ |
旧框架 | 不再接受新测试,需要更新的旧测试应迁移到单元测试或新 API 集成测试套件 |
从仓库结构看,integration/ 下按组件划分子目录,例如 integration/container/、integration/image/、integration/volume/ 等,每个目录下是对应组件的端到端测试;而 integration/internal/ 存放跨组件共享的测试辅助代码(容器/镜像构建工具、环境要求判断、终端输出校验等)。integration-cli/ 虽仍存在大量 docker_*_test.go 文件,但按 TESTING.md 的明确规定,它已被标记为 deprecated,新提交到该目录的测试会被 CI 直接拒绝。
二、编写新测试的规范
2.1 新功能(New Features)的测试要求
TESTING.md 对新功能提出的核心原则是:新增代码首先应由单元测试覆盖。如果一段代码难以用单元测试测试,这恰恰是它需要重构的信号——文档建议将依赖从具体结构体改为非导出接口(unexported interfaces),以便在测试中注入 fake 实现。
集成测试的边界同样被严格限定:
- 若新功能引入了全新的 API 端点,则必须新增一个 API 集成测试,覆盖该端点的成功路径(success case);
- 若只是为已有端点新增 API 字段,应把新字段加到该端点的现有集成测试中,而不是新建测试;
- 不要为每一个新 API 字段或 API 错误分支单独添加集成测试——错误分支(error cases)应由单元测试负责。
2.2 Bug 修复(Bug Fixes)的测试要求
- 修复必须附带一个实际触发该 bug 路径的单元测试用例;
- 如果 bug 涉及某个 API 端点的行为,可以在该端点的现有集成测试中追加新的断言(new assertions),而非新写一个集成测试。
2.3 新集成测试必须放在 integration/ 下
再次强调:integration-cli 中的新测试会被 CI 拒绝,所有新集成测试都应实现在 integration/ 目录下。
2.4 集成测试的环境条件判断:skip.If
集成测试运行环境千差万别(本地 daemon、远程 daemon、rootless 模式、Windows 等),因此新增或修改 integration/ 下的测试时必须考虑环境适配。TESTING.md 给出的做法是使用 gotest.tools/v3/skip 包的 skip.If 让测试按条件运行。官方示例:
package example
import (
"testing"
"gotest.tools/v3/skip"
)
func TestSomething(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon(), "test requires a local daemon")
// your integration test code
}
当检测到远程 daemon 时测试会被跳过。所有可用的环境条件定义在 internal/testutil/environment/environment.go 中。从该源码可以看到 Execution 类型提供了若干条件判断方法,例如:
IsRemoteDaemon()——被测 daemon 是否位于另一台主机(源码注释:“true if the daemon under test is on different host”);IsRootless()——是否启用了 rootless 模式;RuntimeIsWindowsContainerd()——Windows 上是否使用 containerd 运行时。
仓库内的真实用法可参考 integration/build/build_cgroupns_linux_test.go:
skip.If(t, testEnv.DaemonInfo.OSType != "linux")
skip.If(t, testEnv.IsRemoteDaemon())
skip.If(t, !requirement.CgroupNamespacesEnabled())
这展示了典型的三层过滤:操作系统类型 → daemon 位置 → 特性能力(requirement 包来自 integration/internal/requirement/)。
三、运行测试:make 目标与环境变量
3.1 单元测试:make test-unit
基本用法:
make test-unit
或者在 make shell 启动的容器 / 配置好的环境中直接运行 hack/test/unit。
两个可控制测试子集的环境变量:
TESTDIRS——要测试的目录列表,默认./...;TESTFLAGS——传给go test的额外参数;按名称模式过滤时写TESTFLAGS="-test.run TestNameOrPrefix"。
Makefile 中的对应目标确认了这条调用链:
.PHONY: test-unit
test-unit: build ## run the unit tests
$(DOCKER_RUN_DOCKER) hack/test/unit
即 test-unit 先构建 docker-dev 镜像,再在容器内执行 hack/test/unit。
3.2 集成测试:make test-integration
基本用法:
make test-integration
该 make 目标会同时运行 “integration” 与 “integration-cli” 两套套件。相关控制变量:
| 环境变量 | 作用 |
|---|---|
TEST_INTEGRATION_DIR |
指定要构建和运行哪些集成测试目录 |
TEST_SKIP_INTEGRATION |
设置任意值即跳过 integration 套件 |
TEST_SKIP_INTEGRATION_CLI |
设置任意值即跳过 integration-cli 套件 |
TESTFLAGS_INTEGRATION / TESTFLAGS_INTEGRATION_CLI |
分别为两套套件传递专属 flag |
TEST_FILTER |
测试过滤器,直接透传给 go test -run(integration-cli 对应 go test -check-f),并自动联动设置上述其它变量 |
Makefile 展示了跳过逻辑的实现:当 TEST_SKIP_INTEGRATION 和 TEST_SKIP_INTEGRATION_CLI 同时非空时,目标直接打印 “Both integrations suites skipped per environment variables” 并退出;否则进入容器执行 hack/make.sh dynbinary test-integration。而 hack/make/test-integration 脚本则负责实际的测试编排:构建测试二进制(build_test_suite_binaries)→ 启动被测 daemon(bundle .integration-daemon-start)→ 循环执行 run_test_integration → 无论成败都执行 .integration-daemon-stop 与二进制清理,并将全过程输出追加到 test.log。
此外 Makefile 还提供 test-integration-flaky 目标,用于对新集成测试做压力重跑,排查不稳定(flaky)用例;Makefile 的总目标 make test 则会串联 build、test-unit、test-integration 与 test-docker-py。
3.3 指定 Go 版本
通过 GO_VERSION 变量可以切换用于构建被测试代码的 Go 版本,例如:
make GO_VERSION=1.24.8 test
四、源码纵深:hack/test/unit 的测试编排细节
hack/test/unit 脚本揭示了 make test-unit 背后的关键工程细节,这些细节直接影响你使用 TESTDIRS / TESTFLAGS 时的行为:
- api 与 client 是独立 Go 模块,单独执行。脚本开头(hack/test/unit#L26-L54)会判断
TESTDIRS是否命中./api或./client,命中后分别进入对应模块目录,用gotestsum ... -mod=readonly单独运行。原因是仓库存在vendor/目录,若不排除 vendor 模式,api/client模块的测试文件可能不被识别;注释中特别引用了 Go 官方文档对-mod=readonly的解释——忽略 vendor 目录,并在go.mod需要更新时报错。 - 默认排除 vendor 与 integration。主模块的包列表来自
go list "$TESTDIRS" | grep -vE '(/vendor/|/integration)'(hack/test/unit#L91-L95),确保make test-unit不会误跑集成测试。 - libnetwork 强制串行执行。由于 libnetwork 测试会调用 iptables,脚本将其拆出并用
-p=1顺序执行(hack/test/unit#L124-L135)。 - flaky 测试单独重跑。第一轮会用
-skip=TestFlaky.*跳过 flaky 用例,结束后再用--rerun-fails=4对TestFlaky.*单独重跑(hack/test/unit#L137-L148);如果你在TESTFLAGS中显式传了-run,则不会进行这轮单独的 flaky 重跑。 - 统一使用 gotestsum 产出报告。各模块分别输出 JSON/JUnit 报告到
bundles/目录(如bundles/junit-report.xml、bundles/coverage.out),并固定-test.timeout=${TIMEOUT:-5m}超时(hack/test/unit#L16)。 - 构建标签固定为
netgo journald(BUILDFLAGS),这与官方构建保持一致。
对于需要 docker-proxy 的 libnetwork/drivers/bridge 包,脚本还会自动调用 hack/make.sh binary-proxy install-proxy 补装依赖(hack/test/unit#L97-L100)。
五、实践清单:按场景选择测试命令
结合上述机制,日常开发中的典型用法如下(均相对仓库根目录):
# 全量单元测试
make test-unit
# 只测某包(等价于 hack/test/unit 的 TESTDIRS)
make TESTDIRS='./daemon/...' test-unit
# 按名称模式过滤
make TESTFLAGS="-test.run TestContainerCreate" test-unit
# 只跑新集成测试套件,跳过遗留套件
make TEST_SKIP_INTEGRATION_CLI=1 test-integration
# 只跑 integration 下指定组件目录
make TEST_INTEGRATION_DIR="container image" test-integration
# 用过滤器直接筛测试名
make TEST_FILTER="TestLogs" test-integration
# 换 Go 版本跑完整测试
make GO_VERSION=1.24.8 test
适用前提与限制需要留意:这些 make 目标都依赖 build(构建 docker-dev 镜像)并在容器内执行,因此宿主机需要先具备可用的 Docker;TEST_FILTER 对 integration-cli 透传的是 -check-f 而非 -run,两套套件的行为差异在 TESTING.md 中已明确说明。
六、小结
Moby 的测试体系可以归纳为三条主线:
- 分层清晰:单元测试追求快速且只测本包;集成测试面向真实 daemon 走 HTTP 协议;遗留套件
integration-cli/冻结不再扩张(TESTING.md)。 - 编写有章法:新功能必须有单测覆盖、仅全新 API 端点才新增集成测试、错误分支归单元测试;集成测试用
skip.If+ internal/testutil/environment/environment.go 适配环境(TESTING.md)。 - 运行可控:
make test-unit/make test-integration配合TESTDIRS、TESTFLAGS、TEST_INTEGRATION_DIR、TEST_SKIP_INTEGRATION*、TEST_FILTER、GO_VERSION等变量精确控制范围(Makefile),底层由 hack/test/unit 与 hack/make/test-integration 完成多模块、多套件的自动化编排。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00