Go语言精进之路:从新手到高手的编程思想、方法和技巧(2)
上QQ阅读APP看书,第一时间看更新

43.2 golden文件惯用法

在为上面的例子准备预期结果数据文件attendee1.xml时,你可能会有这样的问题:attendee1.xml中的数据从哪里得到?

的确可以根据Attendee的MarshalXML方法的逻辑手动“造”出结果数据,但更快捷的方法是通过代码来得到预期结果。可以通过标准格式化函数输出对Attendee实例进行序列化后的结果。如果这个结果与我们的期望相符,那么就可以将它作为预期结果数据写入attendee1.xml文件中:

got, err := xml.MarshalIndent(&tt.a, "", "  ")
if err != nil {
    ...
}
println(string(got)) // 这里输出XML编码后的结果数据

如果仅是将标准输出中符合要求的预期结果数据手动复制到attendee1.xml文件中,那么标准输出中的不可见控制字符很可能会对最终复制的数据造成影响,从而导致测试失败。更有一些被测目标输出的是纯二进制数据,通过手动复制是无法实现预期结果数据文件的制作的。因此,我们还需要通过代码来实现attendee1.xml文件内容的填充,比如:

got, err := xml.MarshalIndent(&tt.a, "", "  ")
if err != nil {
    ...
}
ioutil.WriteFile("testdata/attendee1.xml", got, 0644)

问题出现了!难道我们还要为每个testdata下面的预期结果文件单独编写一个小型的程序来在测试前写入预期数据?能否把将预期数据采集到文件的过程与测试代码融合到一起呢?Go标准库为我们提供了一种惯用法:golden文件

将上面的例子改造为采用golden文件模式(将attendee1.xml重命名为attendee1.golden以明示该测试用例采用了golden文件惯用法):

// chapter8/sources/testdata-demo2/attendee_test.go
...

var update = flag.Bool("update", false, "update .golden files")

func TestAttendeeMarshal(t *testing.T) {
    tests := []struct {
        fileName string
        a        Attendee
    }{
        {
            fileName: "attendee1.golden",
            a: Attendee{
                Name:  "robpike",
                Age:   60,
                Phone: "13912345678",
            },
        },
    }

    for _, tt := range tests {
        got, err := xml.MarshalIndent(&tt.a, "", "  ")
        if err != nil {
            t.Fatalf("want nil, got %v", err)
        }

        golden := filepath.Join("testdata", tt.fileName)
        if *update {
            ioutil.WriteFile(golden, got, 0644)
        }

        want, err := ioutil.ReadFile(golden)
        if err != nil {
            t.Fatalf("open file %s failed: %v", tt.fileName, err)
        }

        if !bytes.Equal(got, want) {
            t.Errorf("want %s, got %s", string(want), string(got))
        }
    }
}

在改造后的测试代码中,我们看到新增了一个名为update的变量以及它所控制的golden文件的预期结果数据采集过程:

if *update {
    ioutil.WriteFile(golden, got, 0644)
}

这样,当我们执行下面的命令时,测试代码会先将最新的预期结果写入testdata目录下的golden文件中,然后将该结果与从golden文件中读出的结果做比较。

$go test -v . -update
=== RUN   TestAttendeeMarshal
--- PASS: TestAttendeeMarshal (0.00s)
PASS
ok     sources/testdata-demo2   0.006s

显然这样执行的测试是一定会通过的,因为在此次执行中,预期结果数据文件的内容就是通过被测函数刚刚生成的。

但带有-update命令参数的go test命令仅在需要进行预期结果数据采集时才会执行,尤其是在因数据生成逻辑或类型结构定义发生变化,需要重新采集预期结果数据时。比如:我们给上面的Attendee结构体类型增加一个新字段topic,如果不重新采集预期结果数据,那么测试一定是无法通过的。

采用golden文件惯用法后,要格外注意在每次重新采集预期结果后,对golden文件中的数据进行正确性检查,否则很容易出现预期结果数据不正确,但测试依然通过的情况。

小结

在这一条中,我们了解到面向工程的Go语言对测试依赖的外部数据文件的存放位置进行了规范,统一使用testdata目录,开发人员可以采用将预期数据文件放在testdata下的方式为测试提供静态测试固件。而Go golden文件的惯用法实现了testdata目录下测试依赖的预期结果数据文件的数据采集与测试代码的融合。