Go语言 测试练习


Go工程化的语言供简便 实用 单元测试能力强
测试 Hello Test 在$GOPATH/src 目录下创建hello目录 示例代码根目录
建文件hello.go 函数 hello()功能 返回由单词拼接成句子
package hello
func hello() string {
 words := []string{"hello", "func", "in", "package", "hello"}
 wl := len(words)
 sentence := ""
 for key, word := range words {
  sentence += word
  if key < wl-1 {
   sentence += " "
  } else {
   sentence += "."
  }
 }
 return sentence
}
建文件hello_test.go内容    
package hello
import (
 "fmt"
 "testing"
)
func TestHello(t *testing.T) {
 got := hello()
 expect := "hello func in package hello."
  if got != expect {
  t.Errorf("got [%s] expected [%s]", got, expect)
 }
}
func BenchmarkHello(b *testing.B) {
 for i := 0; i < b.N; i++ {
  hello()
 }
}
func ExampleHello() {
 hl := hello()
 fmt.Println(hl) // Output: hello func in package hello.
}
打开终端 进入hello目录 命令 go test
c:/go/bin/go.exe test -v [F:/user/go/src/hello]
PASS
ok    0.665s
Go测试代码 名称以_test.go结尾的
测试代码包含[测试函数 测试辅助代码 示例函数]

测试函数
以Test开头的是功能测试函数
以Benchmark开头的是性能测试函数

测试辅助代码
为测试函数服务的公共函数 初始化函数 测试数据等

示例函数
以Example开头的说明 被测试函数用法的函数
测试代码作为包的一部分
可以访问包中不可导出的元素
但需要的时候(如避免循环依赖)可修改测试文件的包名
如package hello的测试文件 包名可设为 package hello_test

功能测试函数

要接收 *testing.T 类型的单一参数
t *testing.T 类型管理 测试状态和支持格式化的测试日志
测试日志在测试执行过程中积累 完成后输出到标准错误输出
Go标准库testing.T 类型的常用方法的用法
测试函数中的某条测试用例 执行结果与预期不符时
调用 t.Error()或t.Errorf()方法 记录日志 并标记测试失败
#go/src/bytes/compare_test.go
func TestCompareIdenticalSlice(t *testing.T) {
 var b = []byte("Hello Gophers!")
 if Compare(b, b) != 0 {
  t.Error("b != b")
 }
 if Compare(b, b[:1]) != 1 {
  t.Error("b > b[:1] failed")
 }
}

t.Fatal和t.Fatalf方法 失败跳出该测试函数

# go/src/bytes/reader_test.go
func TestReadAfterBigSeek(t *testing.T) {
 r := NewReader([]byte("0123456789"))
 if _, err := r.Seek(1<<31+5, os.SEEK_SET); err != nil {
  t.Fatal(err)
 }
 if n, err := r.Read(make([]byte, 10)); n != 0 || err != io.EOF {
  t.Errorf("Read = %d, %v; want 0, EOF", n, err)
 }
}

t.Skip和t.Skipf方法 跳过某条测试用例

#go/src/archive/zip/zip_test.go
func TestZip64(t *testing.T) {
 if testing.Short() {
  t.Skip("slow test; skipping")
 }
 const size = 1 << 32 // before the "END\n" part
 buf := testZip64(t, size)
 testZip64DirectoryRecordLength(buf, t)
}

测试t.Log和 t.Logf记录日志

#go/src/regexp/exec_test.go
func TestFowler(t *testing.T) {
 files, err := filepath.Glob("testdata[A-Za-z0-9_\-]+\.go:` + Rline
 Rshortfile = `[A-Za-z0-9_\-]+\.go:` + Rline
)
var tests = []tester{ // individual pieces:
 {0, "", ""},
 {0, "XXX", "XXX"},
 {Ldate, "", Rdate + " "},
 {Ltime, "", Rtime + " "},
 {Ltime | Lmicroseconds, "", Rtime + Rmicroseconds + " "},
 {Lmicroseconds, "", Rtime + Rmicroseconds + " "}, // microsec implies time
 {Llongfile, "", Rlongfile + " "},
 {Lshortfile, "", Rshortfile + " "},
 {Llongfile | Lshortfile, "", Rshortfile + " "}, // shortfile overrides longfile
 // everything at once:
 {Ldate | Ltime | Lmicroseconds | Llongfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rlongfile + " "},
 {Ldate | Ltime | Lmicroseconds | Lshortfile, "XXX", "XXX" + Rdate + " " + Rtime + Rmicroseconds + " " + Rshortfile + " "},
}
测试代码中也能定义init函数
init函数会在引入外部包 定义常量 声明变量 后被自动调用
可在init函数里编写测试 的初始化代码
#go/src/bytes/buffer_test.go
func init() {
 testBytes = make([]byte, N)
 for i := 0; i < N; i++ {
  testBytes[i] = 'a' + byte(i%26)
 }
 data = string(testBytes)
}

封装测试专用函数 抽象测试结构体


#go/src/log/log_test.go:
type tester struct {
 flag int
 prefix string
 pattern string // regexp that log output must match; we add ^ and expected_text$ always
}
func testPrint(t *testing.T, flag int, prefix string, pattern string, useFormat bool) {
 // ...
}
无需接收参数 但需用注释Output:标记说明示例函数的输出值
未指定Output:标记或输出值为空的示例不被执行
示例函数需要归属于某个 包/函数/类型/类型 的方法 具体命名规则如下
func Example() { ... }  # 包的示例函数
func ExampleF() { ... }  # 函数F的示例函数
func ExampleT() { ... }  # 类型T的示例函数
func ExampleT_M() { ... } # 类型T的M方法的示例函数
# 多示例函数 需要跟下划线加小写字母开头的后缀
func Example_suffix() { ... }
func ExampleF_suffix() { ... }
func ExampleT_suffix() { ... }
func ExampleT_M_suffix() { ... }
go doc 工具会 解析 示例函数 的 函数体作为对应 包/函数/类型/类型的方法 的用法
测试函数的相关说明 可以通过 go help testfunc 看帮助文档

使用 go test 工具

go test执行测试
shell端进入测试包的目录
go test或go test $pkg_name_in_gopath 即对指定的包测试
如go test github.com/tabalt/... 的命令
是测试$GOPATH/github.com/tabalt/ 目录下所有的项目
go test std 则 执行Golang标准库的所有测试

查看测试结果 -v 参数

go test -v
只执行其中的某些函数 可用-run 参数
正则表达式 匹配 要执行的功能测试函数名
指定参数后 其他功能测试函数TestHello不执行
go test -v -run=xxx

性能测试 加-bench参数

性能测试函数默认不执行 添加-bench参数 并指定匹配性能测试函数名的正则表达式
如要执行 包中所有的性能测试函数 -bench . 或 -bench=.
go test -bench=.
PASS
ok      hello   2.772s
查看 性能测试时的内存情况 添加参数 -benchmem
go test -bench=. -benchmem
PASS
BenchmarkHello-8  2000000   666 ns/op   208 B/op   9 allocs/op
ok  hello 2.014s
参数 -cover 查看 编写的测试对代码的覆盖率
详细覆盖率信息 -coverprofile 输出到文件 并用 go tool cover 来查看
用法参考 go tool cover -help  
go test 命令的参数及用法
通过go help testflag 查看

高级测试技术

IO相关测试
testing/iotest包 实现了常用的出错 Reader和Writer 可供 在io相关的测试中使用
触发数据错误dataErrReader 通过DataErrReader()函数创建
读取一半内容 halfReader 通过HalfReader()函数创建
读取一个byte的oneByteReader 通过OneByteReader()函数创建
触发超时错误的timeoutReader 通过TimeoutReader()函数创建
写入指定位数内容后停止 truncateWriter 通过TruncateWriter()函数创建
读取时记录日志 readLogger 通过NewReadLogger()函数创建
写入时记录日志 writeLogger 通过NewWriteLogger()函数创建

黑盒测试

testing/quick包实现了帮助黑盒测试的实用函数 Check和CheckEqual
Check 函数的第1个参数是要测试的 只返回bool值的黑盒函数
f Check会为f的每个参数设置任意值并多次调用
如果f返回false Check函数会返回错误值 *CheckError
Check函数的第2个参数
可指定quick.Config类型
config传nil则默认用quick.defaultConfig
quick.Config结构体包含测试运行选项
# go/src/math/big/int_test.go
func checkMul(a, b []byte) bool {
 var x, y, z1 Int
 x.SetBytes(a)
 y.SetBytes(b)
 z1.Mul(&x, &y)
 var z2 Int
 z2.SetBytes(mulBytes(a, b))
 return z1.Cmp(&z2) == 0
}
func TestMul(t *testing.T) {
 if err := quick.Check(checkMul, nil); err != nil {
  t.Error(err)
 }
}
CheckEqual函数是比较给定的两个黑盒函数是否相等 函数原型如下
func CheckEqual(f, g interface{}, config *Config) (err error)

HTTP测试

net/http/httptest包提供了HTTP相关代码的工具的测试代码
可创建临时的httptest.Server来测试发送HTTP请求的代码
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintln(w, "Hello, client")
}))
defer ts.Close()
res, err := http.Get(ts.URL)
if err != nil {
 log.Fatal(err)
}
greeting, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
 log.Fatal(err)
}
fmt.Printf("%s", greeting)

创建应答的记录器httptest.ResponseRecorder来检测应答内容
handler := func(w http.ResponseWriter, r *http.Request) {
 http.Error(w, "something failed", http.StatusInternalServerError)
}
req, err := http.NewRequest("GET", "http://example.com/foo", nil)
if err != nil {
 log.Fatal(err)
}
w := httptest.NewRecorder()
handler(w, req)
fmt.Printf("%d - %s", w.Code, w.Body.String())

测试进程操作行为

当被测函数有操作进程的行为 可将被测程序作为一个子进程执行测试
func Crasher() {//被测试的进程退出函数
 fmt.Println("Going down in flames!")
 os.Exit(1)
}
//测试进程退出函数的测试函数
func TestCrasher(t *testing.T) {
 if os.Getenv("BE_CRASHER") == "1" {
  Crasher()
  return
 }
 cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
 cmd.Env = append(os.Environ(), "BE_CRASHER=1")
 err := cmd.Run()
 if e, ok := err.(*exec.ExitError); ok && !e.Success() {
  return
 }
 t.Fatalf("process ran with err %v, want exit status 1", err)
}