Go 1.27 test2json 新增 OutputType:CI 终于能区分测试日志和报错了
标签: Go语言 / Go1.27 / testing / test2json / CI / 工程实践
原文: 微信公众号「源自开发者」https://mp.weixin.qq.com/s/bT4bcJTQ-JmYDA3DeZnQNw
核心亮点
Go 1.27 中,testing 包和 test2json 工具迎来一项实用变化:JSON 测试输出流新增
OutputType字段,区分普通日志、框架信息和错误输出。
持续集成系统终于可以准确判断一行测试输出到底是 t.Log 的调试信息还是 t.Error 的失败原因。
一个长期存在的盲区
如果你用过 go test -json,应该对这样的输出不陌生:
{"Action":"output","Test":"TestParse","Output":" parse_test.go:42: input: \"invalid\"\n"}
{"Action":"output","Test":"TestParse","Output":" parse_test.go:43: got error: syntax error\n"}
{"Action":"output","Test":"TestParse","Output":" parse_test.go:44: expected: syntax error at line 1\n"}
{"Action":"fail","Test":"TestParse","Elapsed":0.01}
这三行 JSON 的 Action 都是 "output",结构完全相同,没有任何标记能告诉消费者哪些是 t.Log 的正常输出,哪些是 t.Error 或 t.Fatal 产生的错误信息。
CI 的困扰
很多持续集成工具在构建测试失败摘要时,只能依赖启发式规则——比如取失败前的最后一行输出作为错误摘要。但这种规则并不可靠:
| 场景 | 问题 |
|---|---|
t.Error 后继续执行 | 后续日志会淹没错误信息 |
| 测试直接 panic | 无法提取有效摘要 |
| 集成测试数千行输出 | 根本原因被淹没 |
结果就是 CI 面板上经常出现**"测试失败,但不知道为什么"**的情况,开发者需要手动翻找完整的测试日志。
这个问题早在 2021 年就被提上 Go 的议题列表,直到 Go 1.27 才真正落地。
实现方式:控制字符标记协议
Go 1.27 在 testing 包和 test2json 之间引入了一套基于控制字符的标记协议。
三个控制字符
| 字符 | ASCII | 含义 |
|---|---|---|
^O | 15 | 标记错误输出开始,对应 t.Error / t.Fatal |
^N | 14 | 标记错误输出结束 |
^V | 22 | 标记测试框架行(=== RUN、--- PASS 等) |
安全机制:如果被测代码本身输出了这些控制字符,testing 包会用 ^[(ASCII 27)进行转义。
OutputType 四种取值
| 值 | 含义 |
|---|---|
空字符串 "" | 普通输出,来自 t.Log 或标准输出 |
"frame" | 测试框架信息行 |
"error" | t.Error / t.Fatal 输出的第一行 |
"error-continue" | 多行错误输出的后续行 |
处理后的输出
{"Action":"output","Test":"TestParse","Output":" parse_test.go:42: input: \"invalid\"\n","OutputType":""}
{"Action":"output","Test":"TestParse","Output":" parse_test.go:43: got error: syntax error\n","OutputType":"error"}
{"Action":"output","Test":"TestParse","Output":" parse_test.go:44: expected: syntax error at line 1\n","OutputType":"error-continue"}
实现细节
实现并不复杂:
- testing 包:在
outputWriter.writeLine方法中判断当前行是否属于错误输出(通过调用栈参数传入的isErr标记),如果是则在前缀加上^O,后缀加上^N - test2json:
Converter在解析过程中跟踪markErrEnd状态,遇到^O标记的行设置OutputType为"error",后续未被^N终止的行标记为"error-continue"
关键:
OutputType描述的是输出行的内容类型,而不是Output字段的一部分。即使日志中包含多行错误,只有属于t.Error/t.Fatal调用产生的行才会被标记。
对工程实践的意义
1. CI 集成
有了 OutputType,CI 系统不再需要猜测哪些输出是错误。以 GitHub Actions 为例:
type TestEvent struct {
Action string `json:"Action"`
Test string `json:"Test,omitempty"`
Output string `json:"Output,omitempty"`
OutputType string `json:"OutputType,omitempty"` // Go 1.27+
Elapsed float64 `json:"Elapsed,omitempty"`
}
// 提取测试失败摘要
func extractFailureSummary(events []TestEvent) string {
var sb strings.Builder
for _, e := range events {
if e.OutputType == "error" || e.OutputType == "error-continue" {
sb.WriteString(e.Output)
}
}
return sb.String()
}
2. 大量集成测试的团队
测试失败时日志可能有数千行,其中大部分是 t.Log 的正常诊断信息。有了准确的错误标记,无论失败发生在测试的哪个阶段,CI 都能精确提取到 t.Error / t.Fatal 的输出位置。
3. 测试工具生态受益
gotestsum等第三方测试运行器可以直接利用OutputType生成更准确的报告- 不再需要自己维护输出解析逻辑
- 可基于
OutputType实现更智能的输出过滤、着色和报告生成
兼容性:完全向后兼容
| 场景 | 影响 |
|---|---|
| 简单日志导入 | 无变化,透明升级 |
| 已有 JSON 消费者 | 不需要修改,OutputType 是可选字段 |
| 空字符串值 | 与 Go 1.27 之前行为一致 |
go tool test2json -p -t | 参数保持不变 |
| 新版测试二进制 + 旧版 test2json | 控制字符出现在 Output 中,实际影响微乎其微 |
写在最后
OutputType 是那种看起来不起眼,但用上之后会让人感叹"为什么现在才有"的变化。
它没有改变 Go 测试的编写方式,也没有引入新的 API,但却让测试输出的消费端有了更准确的判断依据。
对于每天和 CI 面板打交道的团队,这个变化意味着:
- 更少的"测试失败详情"误判
- 更快的故障定位
- 从 Go 1.27 开始,测试输出不再是单纯的文本流,而是一份带注释的结构化事件序列
区别就在那一行 OutputType 上。
本文整理自微信公众号「源自开发者」,原文链接:https://mp.weixin.qq.com/s/bT4bcJTQ-JmYDA3DeZnQNw
Go CL: https://github.com/golang/go/issues/70207