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

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

测试通过是预料之中的事情。