如何在 Go 项目中运行所有测试以及“内部”发生了什么?
\n每个 Go 开发者都曾在终端中输入过神奇的 go test ./... 命令,并看着绿色的勾号在屏幕上滚动。但您是否想过,这一刻到底发生了什么?为什么测试会以这种方式运行,而不是其他方式?为什么有时 go test 会默默地跳过您的新测试?
在本文中,我们不仅会回答 Stack Overflow 上的热门问题(该问题已获得数千次浏览),还会深入 Go 编译器的内部,了解测试机制是如何构建的。开始吧!
\n\n为什么 go test ./... 如此神奇?
\n让我们从基础开始。不带参数的 go test 命令只运行当前包中的测试。但如果您的项目有几十个包(在实际生产环境中通常如此),手动逐个遍历它们无异于中世纪酷刑。这时 ./... 就派上用场了。
这个模式的意思是:“从当前目录开始,递归查找所有包”。Go 会遍历目录树,忽略 vendor 和模块,并收集所有包的列表。对于每个包,它会启动一个独立的 go test 实例。
但是,如果您只想在特定目录中运行测试呢?例如,只在 ./internal/... 中?同样有效:go test ./internal/...。
重要提示:如果您使用 Go Modules(从 Go 1.16+ 开始是标准),那么 ./... 仅在当前模块内有效。要测试工作区中的所有模块,您必须在每个模块中使用 go test ./...,或者配置 go.work。
在 go test 的上下文中,“包”是什么?
\nGo 使用 包 的概念——同一目录中的一组 .go 文件。测试文件(带有 _test.go 后缀)也被视为包的一部分,但会单独编译。当您运行 go test 时,Go 会编译一个测试二进制文件,其中包含:
- \n
- 包的源代码(但仅限测试中使用的部分) \n
- 测试本身的代码 \n
- 用于运行和报告的特殊运行时包装 \n
这个二进制文件会被执行,运行测试并输出结果。整个过程对每个包都是独立的。
\n\nGo 如何决定运行哪些测试?
\n当您输入 go test ./... 时,Go 会执行以下步骤:
- \n
- 遍历文件系统——Go 找到所有包含
.go文件的目录。 \n - 过滤——忽略
vendor、testdata目录和隐藏文件夹(以.开头的)。 \n - 编译——为每个包创建一个临时二进制文件。 \n
- 运行——执行二进制文件,收集结果。 \n
- 缓存——如果代码没有变化,Go 会使用缓存并显示
(cached)。 \n
最后一点是常见的惊讶原因。您修改了测试,运行 go test ./...,但它显示“ok (cached)”。为什么?因为 Go 根据输入文件的哈希值缓存结果。如果您只修改了注释或未使用的导入,哈希值不会改变,测试也不会重新运行。
小技巧:可以使用 go clean -testcache 命令强制清除缓存。或者使用 -count=1 标志,它强制 Go 忽略缓存:go test -count=1 ./...。
并行性:为什么测试并不总是更快?
\n默认情况下,go test 在单个包内顺序运行测试。但不同包之间是并行运行的!同时运行的包数量由 -p 标志决定(默认等于 CPU 数量)。
听起来很棒,但有一个问题: