go test加参数-json就能输出 json格式,下面我们用一个简单的例子看下对他进行分析,然后分析下相关源码。对于单测
package testimport ("fmt""testing")func TestAdd(t *testing.T) {type args struct {a intb int}tests := []struct {name stringargs argswant int}{// TODO: Add test cases.{args: args{a: 1,b: 2,},want: 3,},{args: args{a: 1,b: 2,},want: 4,},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {if got := Add(tt.args.a, tt.args.b); got != tt.want {t.Errorf("Add() = %v, want %v", got, tt.want)}})}}func TestSub(t *testing.T) {type args struct {a intb int}tests := []struct {name stringargs argswant int}{// TODO: Add test cases.{args: args{a: 1,b: 2,},want: -1,},}for _, tt := range tests {fmt.Println("before")fmt.Print("before not endline")t.Run(tt.name, func(t *testing.T) {if got := Sub(tt.args.a, tt.args.b); got != tt.want {t.Errorf("Sub() = %v, want %v", got, tt.want)}})fmt.Println("after")t.Skip("skipping test; 1 not set")}}
执行命令
% go test ./test/gotest/test/... -json
它的输出如下:
{"Time":"2023-05-13T17:27:15.492681+08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestAdd"}{"Time":"2023-05-13T17:27:15.493129+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd","Output":"=== RUN TestAdd\n"}{"Time":"2023-05-13T17:27:15.49316+08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestAdd/#00"}{"Time":"2023-05-13T17:27:15.493168+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#00","Output":"=== RUN TestAdd/#00\n"}{"Time":"2023-05-13T17:27:15.493185+08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestAdd/#01"}{"Time":"2023-05-13T17:27:15.493191+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Output":"=== RUN TestAdd/#01\n"}{"Time":"2023-05-13T17:27:15.493195+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Output":" add_test.go:38: Add() = 3, want 4\n"}{"Time":"2023-05-13T17:27:15.493444+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd","Output":"--- FAIL: TestAdd (0.00s)\n"}{"Time":"2023-05-13T17:27:15.493471+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#00","Output":" --- PASS: TestAdd/#00 (0.00s)\n"}{"Time":"2023-05-13T17:27:15.493477+08:00","Action":"pass","Package":"learn/test/gotest/test","Test":"TestAdd/#00","Elapsed":0}{"Time":"2023-05-13T17:27:15.49349+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Output":" --- FAIL: TestAdd/#01 (0.00s)\n"}{"Time":"2023-05-13T17:27:15.493495+08:00","Action":"fail","Package":"learn/test/gotest/test","Test":"TestAdd/#01","Elapsed":0}{"Time":"2023-05-13T17:27:15.493499+08:00","Action":"fail","Package":"learn/test/gotest/test","Test":"TestAdd","Elapsed":0}{"Time":"2023-05-13T17:27:15.493503+08:00","Action":"run","Package":"learn/test/gotest/test","Test":"TestSub"}{"Time":"2023-05-13T17:27:15.493506+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"=== RUN TestSub\n"}{"Time":"2023-05-13T17:27:15.49362+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"before\n"}{"Time":"2023-05-13T17:27:15.493633+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"before not endline=== RUN TestSub/#00\n"}{"Time":"2023-05-13T17:27:15.493642+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"after\n"}{"Time":"2023-05-13T17:27:15.493646+08:00","Action":"cont","Package":"learn/test/gotest/test","Test":"TestSub"}{"Time":"2023-05-13T17:27:15.49365+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"=== CONT TestSub\n"}{"Time":"2023-05-13T17:27:15.493654+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":" add_test.go:72: skipping test; 1 not set\n"}{"Time":"2023-05-13T17:27:15.493659+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub","Output":"--- SKIP: TestSub (0.00s)\n"}{"Time":"2023-05-13T17:27:15.493802+08:00","Action":"output","Package":"learn/test/gotest/test","Test":"TestSub/#00","Output":" --- PASS: TestSub/#00 (0.00s)\n"}{"Time":"2023-05-13T17:27:15.493818+08:00","Action":"pass","Package":"learn/test/gotest/test","Test":"TestSub/#00","Elapsed":0}{"Time":"2023-05-13T17:27:15.493824+08:00","Action":"skip","Package":"learn/test/gotest/test","Test":"TestSub","Elapsed":0}{"Time":"2023-05-13T17:27:15.493827+08:00","Action":"output","Package":"learn/test/gotest/test","Output":"FAIL\n"}{"Time":"2023-05-13T17:27:15.493899+08:00","Action":"output","Package":"learn/test/gotest/test","Output":"FAIL\tlearn/test/gotest/test\t0.138s\n"}{"Time":"2023-05-13T17:27:15.493915+08:00","Action":"fail","Package":"learn/test/gotest/test","Elapsed":0.139}
可以看到,和直直接输出相比,输出格式变成了json,每条输出都是一个json对象,它包含属性有时间,类型、包、测试方法、以及输出等。比不带-json参数多了很多内容
--- FAIL: TestAdd (0.00s)--- FAIL: TestAdd/#01 (0.00s)add_test.go:38: Add() = 3, want 4beforebefore not endlineafterFAILFAIL learn/test/gotest/test 0.176sFAIL
下面分析下它的源码实现,源码位于src/cmd/internal/test2json/test2json.go输出被定义成了event类型
type event struct {Time *time.Time `json:",omitempty"`Action stringPackage string `json:",omitempty"`Test string `json:",omitempty"`Elapsed *float64 `json:",omitempty"`Output *textBytes `json:",omitempty"`}
其中Converter负责将gotest的输出转化为event事件
type Converter struct {w io.Writer // JSON output streampkg string // package to name in eventsmode Mode // mode bitsstart time.Time // time converter startedtestName string // name of current test, for output attributionreport []*event // pending test result reports (nested for subtests)result string // overall test result if seeninput lineBuffer // input bufferoutput lineBuffer // output buffer}
构造方法如下
func NewConverter(w io.Writer, pkg string, mode Mode) *Converter {c := new(Converter)*c = Converter{w: w,pkg: pkg,mode: mode,start: time.Now(),input: lineBuffer{b: make([]byte, 0, inBuffer),line: c.handleInputLine,part: c.output.write,},output: lineBuffer{b: make([]byte, 0, outBuffer),line: c.writeOutputEvent,part: c.writeOutputEvent,},}return c}
退出对应了pass和fail两种action
func (c *Converter) Exited(err error) {if err == nil {c.result = "pass"} else {c.result = "fail"}}
相应的文案定义如下,也就是我们常看到的单测结果
updates = [][]byte{[]byte("=== RUN "),[]byte("=== PAUSE "),[]byte("=== CONT "),}reports = [][]byte{[]byte("--- PASS: "),[]byte("--- FAIL: "),[]byte("--- SKIP: "),[]byte("--- BENCH: "),}
根据输入的每一行内容,进行解析,得到event,如果都没有匹配成功,就把前一个测试的名字赋值给当前测试。否则根据冒号截取内容获取Action和Test。最后把原始输入,写入到Output字段里面。
func (c *Converter) handleInputLine(line []byte) {// Final PASS or FAIL.if bytes.Equal(line, bigPass) || bytes.Equal(line, bigFail) || bytes.HasPrefix(line, bigFailErrorPrefix) {c.flushReport(0)c.output.write(line)if bytes.Equal(line, bigPass) {c.result = "pass"} else {c.result = "fail"}return}if bytes.HasPrefix(line, skipLinePrefix) && bytes.HasSuffix(line, skipLineSuffix) && len(c.report) == 0 {c.result = "skip"}for _, magic := range reports {if bytes.HasPrefix(line, magic) {actionColon = trueok = truebreak}}if indent > 0 && indent <= len(c.report) {c.testName = c.report[indent-1].Test}action := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(string(line[4:i])), ":"))name := strings.TrimSpace(string(line[i:]))c.output.write(origLine)
大于当前层级的结果输出
func (c *Converter) flushReport(depth int) {c.testName = ""for len(c.report) > depth {e := c.report[len(c.report)-1]c.report = c.report[:len(c.report)-1]c.writeEvent(e)}}
输出output是一个单独的事件
func (c *Converter) writeOutputEvent(out []byte) {c.writeEvent(&event{Action: "output",Output: (*textBytes)(&out),})}
最后来到了writevent函数,把包名字和测试名字赋值,然后序列化。加入换行然后输出。
func (c *Converter) writeEvent(e *event) {e.Package = c.pkgif c.mode&Timestamp != 0 {t := time.Now()e.Time = &t}if e.Test == "" {e.Test = c.testName}js, err := json.Marshal(e)js = append(js, '\n')c.w.Write(js)
总的来说,就是解析字符串,转化成json的一个过程。