登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

go test -json 输出怎样区分测试事件和构建事件

来源:17golang原创

时间:2026-10-13 02:05:44 173浏览 收藏

go test -json 的关键不是把每一行都当成测试结果,而是先看 Action。 测试侧通常出现 start、run、pass、fail、skip 和 output;构建侧则使用 build-output 或 build-fail。因此,解析器应该先按 Action 分流,再用 Package、Test 和 FailedBuild 建立关联。

官方命令文档说明,go test -json 会输出换行分隔的 JSON 事件,同时包含测试输出和构建输出。多包测试时事件可能交错,不能用“上一行属于哪个测试”这种隐含状态猜测归属。

官方地址:https://go.dev/cmd/go/

go test -json 事件按 Action 分为测试事件和构建事件的结构说明图
图1:go test -json 事件分流说明图,展示测试事件和构建事件如何从同一条 JSON 流中区分。这是静态说明图,不是运行截图或执行证据。

先记住 Action 的两组边界

可以把 JSON 行先解码成一组兼容字段,再用 Action 判断它来自哪条语义通道。测试事件遵循 cmd/test2json 的 TestEvent 约定;构建事件遵循 BuildEvent 约定。两者都有 Action 和 Output,这正是最容易误判的地方。

Action所属侧解析重点
start、run、pause、cont测试测试生命周期,通常用 Package 和 Test 关联
pass、fail、skip、bench测试结果事件,包级事件可能没有 Test
output测试测试程序输出;不要把它当成构建输出
build-output构建构建工具输出片段,可能出现多行或多条事件
build-fail构建构建失败;重点看 ImportPath 或关联的 FailedBuild

这里有一个实用结论:Action == "output" 只能说明这是测试输出事件,而 Action == "build-output" 才是构建输出事件。不能因为两者都有 Output 字段,就把所有输出拼到同一条测试记录中。

用 go test -json 产生换行事件

先在一个 Go 模块目录执行下面的命令,把每行 JSON 保存下来。./... 方便观察多包场景下的交错事件;实际项目也可以替换成具体包路径。

# 把多包测试事件保存为换行分隔的 JSON,便于后续逐行 Decode
go test -json ./... > test-events.jsonl

# 需要让每次测试都真正运行时,可关闭测试结果缓存
go test -count=1 -json ./... > test-events.jsonl

命令文档把 -json 定义为适合自动处理的 JSON 输出,并明确指出它还会输出构建侧 JSON。文件名使用 .jsonl 是为了提醒自己:这是多个 JSON 对象逐行排列的流,不是一个外层数组。

用一个结构体兼容两类事件

解析时不必先判断 JSON 属于哪一个结构体,因为两种事件共享一部分字段。下面的结构体保留本题需要的字段:测试侧的 Package、Test 和 FailedBuild,以及构建侧的 ImportPath。

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "io"
    "os"
)

// Event 同时容纳 TestEvent 和 BuildEvent 的公共字段与定位字段。
type Event struct {
    Action     string  `json:"Action"`
    Package    string  `json:"Package"`
    Test       string  `json:"Test"`
    ImportPath string  `json:"ImportPath"`
    Output     string  `json:"Output"`
    FailedBuild string `json:"FailedBuild"`
    Elapsed    float64 `json:"Elapsed"`
}

func main() {
    // 默认从文件读取;传入 - 时改为从标准输入读取,方便接入管道。
    var input io.Reader = os.Stdin
    if len(os.Args) > 1 && os.Args[1] != "-" {
        file, err := os.Open(os.Args[1])
        if err != nil {
            panic(fmt.Errorf("打开事件文件失败: %w", err))
        }
        defer file.Close() // 处理完成后及时释放文件描述符
        input = file
    }

    scanner := bufio.NewScanner(input)
    // 单条 Output 可能比默认 64 KiB 更长,扩大扫描缓冲区避免截断。
    scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
    line := 0
    for scanner.Scan() {
        line++
        var event Event
        if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
            return fmt.Errorf("第 %d 行不是有效 JSON: %w", line, err)
        }

        switch event.Action {
        case "build-output", "build-fail":
            // BuildEvent 用 ImportPath 标识被构建的包。
            fmt.Printf("BUILD %-12s %s\n", event.Action, event.ImportPath)
        case "start", "run", "pause", "cont", "pass", "fail", "skip", "bench", "output":
            // TestEvent 通过 Package 和可选的 Test 关联到测试对象。
            target := event.Package
            if event.Test != "" {
                target += "." + event.Test
            }
            fmt.Printf("TEST  %-12s %s\n", event.Action, target)
        default:
            // 保留未知 Action,避免未来 Go 增加事件类型时静默丢失。
            fmt.Printf("OTHER %-12s\n", event.Action)
        }
    }
    if err := scanner.Err(); err != nil {
        panic(fmt.Errorf("读取事件流失败: %w", err))
    }
}

示例的分类条件只依赖 Action,这能避免把构建期间的编译器文本误算成测试日志。ImportPath 对构建事件有意义,而测试事件通常看 Package;两者不要混为同一个字段。

Package 和 Test 怎样组成稳定关联

Package 表示正在测试的包;当事件对应某个测试、示例或基准函数时,Test 会给出函数名。包级整体结果没有 Test,例如一个包最终 pass 或因为没有测试而 skip。因此聚合键可以写成“Package + 可选 Test”,但不要强行给空 Test 填一个虚构名称。

go test -json 的 Package、Test、Action 和 FailedBuild 字段关系说明图
图2:go test -json 字段关系结构图,展示包级事件、测试级事件和构建失败定位字段。这是静态说明图,不是运行截图或执行证据。

可以按下面的规则落地:

  • 有 Test:归入具体测试、示例或基准函数。
  • 没有 Test 但有 Package:归入包级生命周期或包级结果。
  • 有 FailedBuild:说明测试失败是由构建失败导致,应把失败包记录为构建根因。
  • 是 build-output 或 build-fail:优先使用 ImportPath,不依赖 Package。

构建失败时不要只统计 fail

构建失败会让流水线看起来像“测试失败”,但处理动作通常不同:测试失败需要定位断言或测试逻辑,构建失败需要先修复语法、依赖、类型或编译条件。官方格式提供了 FailedBuild,它在由构建失败引起的测试 fail 事件中给出失败构建包的包 ID。

// classifyFailure 区分测试逻辑失败和构建导致的失败。
func classifyFailure(event Event) string {
    if event.Action == "build-fail" {
        return "build-failure:" + event.ImportPath
    }
    if event.Action == "fail" && event.FailedBuild != "" {
        // FailedBuild 指向真正没有构建成功的包,而不是当前测试包名。
        return "test-blocked-by-build:" + event.FailedBuild
    }
    if event.Action == "fail" {
        target := event.Package
        if event.Test != "" {
            target += "." + event.Test
        }
        return "test-failure:" + target
    }
    return "not-a-failure"
}

这里不要把 FailedBuild 当作普通错误文本解析。它是结构化定位字段,适合直接写入聚合结果;真正的编译器细节仍在对应的 Output 事件中。

把事件聚合成可靠的最小结果

如果目标只是生成测试摘要,建议至少保存四类数据:事件原始行、Action、测试关联键、构建关联键。输出文本可以按同一类事件分别拼接,因为官方说明中的 Output 是一段片段,可能需要多条事件拼接后才完整。

处理对象判断字段保存字段
测试通过/失败Action 为 pass/fail 且无 FailedBuildPackage、Test、Elapsed
构建输出Action == "build-output"ImportPath、Output
构建失败Action == "build-fail" 或 FailedBuild 非空失败包、原始输出和失败来源
普通测试日志Action == "output"Package、可选 Test、Output

还要为未知字段和标准错误留余地。官方文档提醒,即使使用 -json,标准错误仍可能出现非 JSON 文本,通常代表较早或较严重的错误。因此,采集脚本不要假设标准输出和标准错误永远都是同一种格式。

最后用这张清单检查解析器

  1. 是否用 Action 先区分 build-output/build-fail 与测试事件?
  2. 是否把 output 与 build-output 分开保存?
  3. 是否允许事件交错,而不是依赖上一条 JSON 行推断归属?
  4. 是否允许包级事件没有 Test?
  5. 是否用 FailedBuild 标记“测试被构建失败阻断”的情况?
  6. 是否按事件片段拼接 Output,并保留原始事件以便回溯?

一句话总结:Action 决定事件属于测试还是构建,Package 和 Test 决定测试对象,ImportPath 与 FailedBuild 负责构建失败定位。把这三层关系分开,解析 go test -json 就不会再把编译日志、测试日志和最终结果混成一条状态。

相关问题

  • go test -json 输出是一整个 JSON 数组吗? 不是,它是换行分隔的 JSON 事件流,适合逐行读取。
  • 为什么测试事件的 Package 和构建事件的 ImportPath 不直接相同? 两者服务于不同对象;构建事件标识被构建包,测试事件标识正在测试的包。
  • Action 为 fail 就一定是断言失败吗? 不一定,检查 FailedBuild 是否非空,可能是构建失败导致测试无法执行。
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>