43.1 testdata目录
Go语言规定:Go工具链将忽略名为testdata的目录。这样开发者在编写测试时,就可以在名为testdata的目录下存放和管理测试代码依赖的数据文件。而go test命令在执行时会将被测试程序包源码所在目录设置为其工作目录,这样如果要使用testdata目录下的某个数据文件,我们无须再处理各种恼人的路径问题,而可以直接在测试代码中像下面这样定位到充当测试固件的数据文件:
f, err := os.Open("testdata/data-001.txt")
考虑到不同操作系统对路径分隔符定义的差别(Windows下使用反斜线“\”,Linux/macOS下使用斜线“/”),使用下面的方式可以使测试代码更具可移植性:
f, err := os.Open(filepath.Join("testdata", "data-001.txt"))
在testdata目录中管理测试依赖的外部数据文件的方式在标准库中有着广泛的应用。在$GOROOT/src路径下(Go 1.14):
$find . -name "testdata" -print ./cmd/vet/testdata ./cmd/objdump/testdata ./cmd/asm/internal/asm/testdata ... ./image/testdata ./image/png/testdata ./mime/testdata ./mime/multipart/testdata ./text/template/testdata ./debug/pe/testdata ./debug/macho/testdata ./debug/dwarf/testdata ./debug/gosym/testdata ./debug/plan9obj/testdata ./debug/elf/testdata
以image/png/testdata为例,这里存储着png包测试代码用作静态测试固件的外部依赖数据文件:
$ls benchGray.png benchRGB.png invalid-palette.png benchNRGBA-gradient.png gray-gradient.interlaced.png invalid-trunc.png benchNRGBA-opaque.png gray-gradient.png invalid-zlib.png benchPaletted.png invalid-crc32.png pngsuite/ benchRGB-interlace.png invalid-noend.png $ls testdata/pngsuite README basn2c08.png basn4a16.png ftbgn3p08.png README.original basn2c08.sng basn4a16.sng ftbgn3p08.sng ... basn0g16.sng basn4a08.sng ftbgn2c16.sng ftp1n3p08.sng
png包的测试代码将这些数据文件作为输入,并将经过被测函数(如png.Decode等)处理后得到的结果数据与预期数据对比:
// $GOROOT/src/image/png/reader_test.go var filenames = []string{ "basn0g01", "basn0g01-30", "basn0g02", ... } func TestReader(t *testing.T) { names := filenames if testing.Short() { names = filenamesShort } for _, fn := range names { // 读取.png文件 img, err := readPNG("testdata/pngsuite/" + fn + ".png") if err != nil { t.Error(fn, err) continue } ... // 比较读取的数据img与预期数据 } ... }
我们还经常将预期结果数据保存在文件中并放置在testdata下,然后在测试代码中将被测对象输出的数据与这些预置在文件中的数据进行比较,一致则测试通过;反之,测试失败。来看一个例子:
// chapter8/sources/testdata-demo1/attendee.go type Attendee struct { Name string Age int Phone string } func (a *Attendee) MarshalXML(e *xml.Encoder, start xml.StartElement) error { tokens := []xml.Token{} tokens = append(tokens, xml.StartElement{ Name: xml.Name{"", "attendee"}}) tokens = append(tokens, xml.StartElement{Name: xml.Name{"", "name"}}) tokens = append(tokens, xml.CharData(a.Name)) tokens = append(tokens, xml.EndElement{Name: xml.Name{"", "name"}}) tokens = append(tokens, xml.StartElement{Name: xml.Name{"", "age"}}) tokens = append(tokens, xml.CharData(strconv.Itoa(a.Age))) tokens = append(tokens, xml.EndElement{Name: xml.Name{"", "age"}}) tokens = append(tokens, xml.StartElement{Name: xml.Name{"", "phone"}}) tokens = append(tokens, xml.CharData(a.Phone)) tokens = append(tokens, xml.EndElement{Name: xml.Name{"", "phone"}}) tokens = append(tokens, xml.StartElement{Name: xml.Name{"", "website"}}) tokens = append(tokens, xml.CharData("https://www.gophercon.com/speaker/"+ a.Name)) tokens = append(tokens, xml.EndElement{Name: xml.Name{"", "website"}}) tokens = append(tokens, xml.EndElement{Name: xml.Name{"", "attendee"}}) for _, t := range tokens { err := e.EncodeToken(t) if err != nil { return err } } err := e.Flush() if err != nil { return err } return nil }
在attendee包中,我们为Attendee类型实现了MarshalXML方法,进而实现了xml包的Marshaler接口。这样,当我们调用xml包的Marshal或MarshalIndent方法序列化上面的Attendee实例时,我们实现的MarshalXML方法会被调用来对Attendee实例进行xml编码。和默认的XML编码不同的是,在我们实现的MarshalXML方法中,我们会根据Attendee的name字段自动在输出的XML格式数据中增加一个元素:website。
下面就来为Attendee的MarshalXML方法编写测试:
// chapter8/sources/testdata-demo1/attendee_test.go func TestAttendeeMarshal(t *testing.T) { tests := []struct { fileName string a Attendee }{ { fileName: "attendee1.xml", 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) } want, err := ioutil.ReadFile(filepath.Join("testdata", tt.fileName)) 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)) } } }
接下来,我们将预期结果放入testdata/attendee1.xml中:
// testdata/attendee1.xml <attendee> <name>robpike</name> <age>60</age> <phone>13912345678</phone> <website>https://www.gophercon.com/speaker/robpike</website> </attendee>
执行该测试:
$go test -v . === RUN TestAttendeeMarshal --- PASS: TestAttendeeMarshal (0.00s) PASS ok sources/testdata-demo1 0.007s
测试通过是预料之中的事情。