go test -json 是自定义 reporter 唯一可靠输入源,输出 json lines 流,含 time、action、test、output 等字段;必须按行解析并跳过空行和注释,结合状态机跟踪测试生命周期。

Go test -json 输出结构是自定义 reporter 的唯一可靠输入源
Go 官方不提供 hook 机制让测试框架直接注入 reporter,go test 唯一稳定、可解析的机器可读输出是 -json 模式。它输出标准 JSON 流(每行一个 JSON 对象),包含 Time、Action、Test、Output 等字段。任何自定义 reporter 都必须基于解析该流实现,而不是尝试劫持 t.Log() 或重写 testing.T —— 这些在子测试、并发或第三方测试库中极易失效。
常见错误是试图用 os.Stdout 重定向捕获默认输出,但 go test 的非-json 输出是面向人类的、无结构的,且会混入 color escape codes 和进度提示,无法可靠解析。
用 go tool test2json 将普通 test 输出转为 JSON 流
如果你不能强制使用 go test -json(比如 CI 环境只允许调用 go test 而不许加参数),可以用 go tool test2json 作为中间层:
go test -v | go tool test2json
注意:test2json 默认假设输入是 go test -v 格式;若输入不含 -v,它可能丢弃部分事件。它也**不支持**解析彩色输出(如 ginkgo 或 testify 的自定义日志),此时必须改用 -json 原生模式。
实操建议:
- 始终优先用
go test -json ./... > report.jsonl生成原始数据,再离线处理 - 流式处理时用
go test -json | your-reporter,避免缓冲导致 hang 住(Go 的os.Stdin是行缓冲,但需确保你的 parser 不卡在 partial line) -
test2json输出的Action字段值包括run、output、pass、fail、skip,其中output行的Output字段含t.Log()和失败堆栈,但不含颜色控制符
解析 JSON 流时必须按行读取并容忍空行和注释
go test -json 输出是 JSON Lines(JSONL)格式:每行一个合法 JSON 对象,但 Go 的 encoding/json 不能直接解码多对象流。你得手动按行读、逐行 json.Unmarshal。
容易踩的坑:
- 某些 Go 版本(如 1.21+)会在 JSONL 流开头插入空行或注释行(如
// {"Time":"..."}),不跳过会导致invalid character '/'错误 - 测试超时或 panic 可能导致某条记录缺失
Test字段(例如只有output+fail),需对字段做零值检查 -
Output字段内容自带换行符,直接打印会破坏 reporter 的行格式;如需内联显示,得用strings.TrimSpace或strings.ReplaceAll(Output, "\n", " ")
示例关键逻辑片段:
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "//") {
continue
}
var event struct {
Action string `json:"Action"`
Test string `json:"Test"`
Output string `json:"Output"`
}
if err := json.Unmarshal([]byte(line), &event); err != nil {
// 忽略解析失败行(如 malformed output)
continue
}
// 处理 event...
}
输出格式定制的关键在于状态机而非模板渲染
自定义 reporter 不是简单地把 JSON 字段塞进字符串模板。因为 go test 的 JSONL 是异步事件流(例如:先 run A,再 output A,再 pass A,再 run B),你需要维护一个轻量状态机来跟踪每个测试的生命周期。
典型状态字段包括:currentTest(当前正在执行的测试名)、testStartTimes(map[string]time.Time)、failedTests([]string)。否则你会遇到:
- 嵌套测试(
t.Run("inner", ...))的Output事件没有Test字段,只能靠缩进或父名推断归属 -
pass/fail事件不带耗时,必须靠run和pass时间差计算 - 并发测试下多个
run交错出现,仅靠时间戳无法准确配对
所以最简健壮做法是:只响应 run(记录开始)、output(追加日志)、pass/fail/skip(结束并输出汇总)。其它动作(如 bench)可忽略。
复杂点在于:Go 测试的 “测试名” 是点分路径(PkgName.TestName/CaseName),而 reporter 通常要折叠显示层级。这需要你自己按 / 分割并维护树状结构 —— 标准库不提供,也没法靠正则偷懒,因为测试名本身允许含 /(虽然不推荐)。











