忽视iotest测试工具包
iotest包 https://pkg.go.dev/testing/iotest 提供了测试 readers和writers 函数集合。很多Gopher不知道有这个包,本文讲如何使用该包,方便我们编写测试代码。
当我们编写的程序实现了 io.Reader
接口,记得可以使用iotest.TestReader
来进行测试,该函数用来测试reader行为是否正常:读取到的字节数和读取的内容是否正确,此外它还可以测试io.ReaderAt
行为,如果reader实现了io.ReaderAt
接口,也可以用iotest包提供的函数进行测试。
定义一个LowerCaseReader
结构体,该结构体实现了io.Read
方法,TestLowerCaseReader
测试代码用于测试LowerCaseReader
行为是否正常。测试时使用 iotest.TestReader
验证LowerCaseReader
读取的内容是否符合预期,该函数接收两个参数,参数1为io.Reader
接口,参数2为读取的内容。对应到下面程序,参数1就是&LowerCaseReader
,它实现io.Reader
接口,参数2为[]byte("aBcDeFgHiJ")
。LowerCaseReader
实现的Read方法,直接将从l.reader读取的内容写入到p中,提供给LowerCaseReader的reader是strings.NewReader("aBcDeFgHiJ")
,所以直接验证输入的内容和读取的内容是否相同判断程序工作是否正常。
type LowerCaseReader struct {
reader io.Reader
}
func (l LowerCaseReader) Read(p []byte) (int, error) {
return l.reader.Read(p)
}
编写测试代码验证上面的程序是否正确
代码语言:javascript复制func TestLowerCaseReader(t *testing.T) {
err := iotest.TestReader(
&LowerCaseReader{reader: strings.NewReader("aBcDeFgHiJ")},
[]byte("aBcDeFgHiJ"),
)
if err != nil {
t.Fatal(err)
}
}
执行测试程序输出结果如下:
代码语言:javascript复制=== RUN TestLowerCaseReader
--- PASS: TestLowerCaseReader (0.00s)
PASS
另一个使用iotest
包的场景是使用它来测试我们的应用程序,像 iotest
提供的 ErrReader可以模拟Read时产生错误。下面列举的一些Reader和Writer可以很方便测试我们的业务程序。
- iotest.ErrReader 创建一个返回0byte并且非空error的io.Reader.
- iotest.HalfReader 创建一个io.Reader,该reader从给定的入参reader中读取一半的内容.
- iotest.OneByteReader 创建一个io.Reder,该reader从给定的入参reader中读取一个字节数据.
- iotest.TimeoutReader 创建一个io.Reader,该reader在第二次读取时返回ErrTimeout,没有数据,随后的读取调用正常返回.
- iotest.TruncateWriter 创建一个io.Writer,该writer在写入n个字节后,自动终止写入.
假设我们要实现一个从reader中读取所有字节的功能函数,实例代码如下。现在需要验证这段代码的可靠性,像在读取过程中失败(模拟网络故障).
代码语言:javascript复制func foo(r io.Reader) error {
b, err := io.ReadAll(r)
if err != nil {
return err
}
// ...
}
像这种模拟超时情况,直接使用iotest包提供的iotest.TimeoutReader
,该reader会在第二次读取时返回失败,测试代码运行的结果符合预期。
func randomString(i int) string {
return string(make([]byte, i))
}
func TestFoo(t *testing.T) {
err := foo(iotest.TimeoutReader(
strings.NewReader(randomString(1024)),
))
if err != nil {
t.Fatal(err)
}
}
有了上述知识,现在实现一个自定义的 readAll
函数,与io.ReadAll不同的是,它能够容忍n次错误。具体实现如下,处理逻辑与io.ReadAll类似,不同点是它支持自定义的错误次数,读取过程中产生的错误数小于给定的限制时,不会终止读取。
func readAll(r io.Reader, retries int) ([]byte, error) {
b := make([]byte, 0, 512)
for {
if len(b) == cap(b) {
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b) n]
if err != nil {
if err == io.EOF {
return b, nil
}
retries--
if retries < 0 {
return b, err
}
}
}
}
将foo的内部实现换成上述的 readAll, 此时再用 iotest.TimeoutReader 测试时不会返回错误。
代码语言:javascript复制func foo(r io.Reader) error {
b, err := readAll(r, 3)
if err != nil {
return err
}
// ...
}
总结,编写有 io.Reader 和 io.Writer 相关代码时,记得使用 iotest 提供的功能快速进行测试, 该包提供了很多实用的功能,测试各种读取操作中的异常情况。