Go 语言 单元测试


测试文件及目录
Go 语言 testing测试包 测试文件名称 以 _test.go 结尾
该测试文件 crypt_test.go 与待测试文件crypt.go 同一目录
./service/crypt.go
./service/crypt_test.go

Go 语言测试目录

各个文件中的函数 在该目录中创建文件 _test.go  即可在该文件中调用 其他文件的函数 进行测试
测试函数
_test.go 通过编写测试函数 进行测试
引入 testing包
import (
    "fmt"
    "testing"
)

Go 语言测试函数名

以 Test 开头 且其后的字符串的第一个字符必须是大写 或者 数字
如果没有按照此规则进行命名 则该函数在测试时不会被执行

Go 语言测试函数的参数

必须为(t *testing.T) 该参数包含有 Log 和 Error 等方法 用于输出测试结果
Log 用于测试通过的情况
Error 用于测试出错的情况
如测试函数
func TestUserLogin_normal(t *testing.T) {
    err := User().Login("test1", "321")
    if err == nil {
        t.Log("Login test1 success")
    } else {
        t.Error(err)
    }
}
该函数负责测试User().Login() 方法的功能
功能正常执行 返回nil 则调用t.Log()输出测试通过的信息
测试不通过 调用t.Error()输出错误信息

Go 语言执行测试

测试文件的目录下 命令 go test 即进行测试
假如测试文件中有以下函数
func TestUserRegister_normal(t *testing.T){.....}
func TestUserRegister_IllegalName(t *testing.T){.....}
func TestUserRegister_DuplicateName(t *testing.T){.....}
func TestUserLogin_normal(t *testing.T){.....}
func TestUserLogin_NullUser(t *testing.T){.....}
直接执行go test时 会默认执行所有 Test 开头的函数
执行的顺序与函数定义的顺序相同
如果需要指定某个函数进行测试 可以在命令后加上-run 参数
该参数用于匹配要执行的测试函数的函数名 通过正则表达式来匹配
比如要单独执行 TestUserRegister_normal 测试函数时
go test -run=TestUserRegister_normal
当要执行所有名称前缀为 TestUserRegister 测试函数时
go test -run=TestUserRegister
Go 语言开发工具中 go test 测试 更为快捷
可以直接指定执行某个测试函数
只要点击左侧的运行按钮即可执行该测试
也可以直接点击右上角的运行按钮来执行某个测试文件中的所有测试
但首先要指定待测试的目录 在运行按钮左侧的下拉列表中选择 Edit Configurations 即可进行设置
比如要测试service目录 则将 Directory 设为相应路径 再选择 gotest
执行test命令后 相应的测试函数会按顺序执行一次
如果 测试函数调用了t.Error() 则测试不通过 可以看到输出中显示 Failed 否则显示 Pass 且会显示通过的测试数量以及相应的输出信息

testing 单元测试

testing 为 Go 语言 package 提供自动化测试的支持 通过 go test 命令 能够自动执行如下形式的任何函数
func TestXxx(*testing.T)  可Xxx是任何字母数字字符串 但是第一个字母不能是小些字母
在这函数中 使用 Error, Fail 或相关方法来发出失败信号
要编写一个新的测试套件 需要创建一个名称以 _test.go 结尾的文件 该文件包含 TestXxx 函数
将该文件放在与被测试的包相同的包中 该文件将被排除在正常的程序包之外 在运行 go test 命令时将被包含
详细信息 go help test  和 go help testflag 了解
可以调用 *T 和 *B 的 Skip 方法 跳过该测试或基准测试
func TestTimeConsuming(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping test in short mode.")
    }
    ...
}
第一个单元测试
func Fib(n int) int {
  if n < 2 {   return n
  }
  return Fib(n-1) + Fib(n-2)
}
func TestFib(t *testing.T) {
  var (  in       = 7
      expected = 13
  )
  actual := Fib(in)
  if actual != expected {
      t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
  }
}
$ go test .
ok  simple_code/jquery1.519s
表示测试通过
将 Sum 函数改为
func Fib(n int) int {
  if n < 2 {
     return n
  }
  return Fib(n-1) + Fib(n-1)
}
$ go test .
--- FAIL TestSum (0.00s)
    t_test.go:16 Fib(10) = 64; expected 13
FAIL
FAIL    chapter09/testing    0.009s

Go 语言Table-Driven Test

测试讲究 case 覆盖 当要覆盖更多 case 时 可以采用 Table-Driven  方式写测试 标准库中有很多测试是使用这种方式写的
func TestFib(t *testing.T) {
    var fibTests = []struct {
        in       int // input
        expected int // expected result
    }{
        {1, 1},
        {2, 1},
        {3, 2},
    }
    for _, tt := range fibTests {
        actual := Fib(tt.in)
        if actual != tt.expected {
            t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
        }
    }
}因为使用的是 t.Errorf 其中某个 case 失败 并不会终止测试执行

Go 语言 T 类型

单元测试 传递给测试函数的参数是 *testing.T 类型 用于管理测试状态 支持格式化测试日志
测试日志会在执行测试的过程 不断累积  在测试完成时转储至标准输出
当 测试函数返回时 或 测试函数调用 FailNow、 Fatal、Fatalf、SkipNow、Skip 或  Skipf 中 任意一个时
该测试 宣告结束 跟 Parallel 方法一样  这些方法只能在运行测试函数的 goroutine 中调用
至于其他报告方法 比如 Log 以及 Error 的变种  则可以在多个 goroutine 中同时进行调用
报告方法
方法 带f 是格式化的 格式化语法 参考 fmt 包
T 类型内嵌了 common 类型 common 提供一系列方法 常用到的 这里说的测试中断 都是指当前测试函数  
当 遇到断言错误时  标识这个测试失败 会使用到
Fai      测试失败 测试继续 就是之后的代码依然会执行
FailNow  测试失败 测试中断
FailNow 方法实现的内部 是通过调用 runtime.Goexit() 来中断测试的
当遇到断言错误 只希望跳过这个错误 但是不希望标识测试失败 会使用到 SkipNow 跳过测试 测试中断
SkipNow 方法实现的内部 是通过调用 runtime.Goexit() 来中断测试的
当只希望打印信息 会用到 Log  输出信息
Logf 输出格式化的信息
默认情况下 单元测试成功时 它们打印的信息不会输出  通过加上 -v 选项 输出这些信息 但对于基准测试 它们总是会被输出
当希望跳过这个测试 并且打印出信息 会用到 Skip  相当于 Log + SkipNow
Skipf 相当于 Logf + SkipNow
当希望断言失败时 标识测试失败 并打印出必要的信息 但是测试继续 会用到 Error 相当于 Log + Fail
Errorf 相当于 Logf + Fail
当希望断言失败的时候 标识测试失败 打印出必要的信息 但中断测试 会用到 Fatal 相当于 Log + FailNow
Fatalf 相当于 Logf + FailNow

Go 语言 Parallel 测试

Parallel 方法 用于表示当前测试只会与其他带有 Parallel 方法的测试 并行进行测试
演示Parallel 的使用
var (
    data   = make(map[string]string)
    locker sync.RWMutex
)
func WriteToMap(k, v string) {
    locker.Lock()
    defer locker.Unlock()
    data[k] = v
}
func ReadFromMap(k string) string {
    locker.RLock()
    defer locker.RUnlock()
    return data[k]
}
var pairs = []struct {
    k string
    v string
}{
  {"polaris", "key"},
  {"studygolang", "Go语言"},
  {"stdlib", "Go语言标准库"},
}
//  TestWriteToMap 需要在 TestReadFromMap 之前
func TestWriteToMap(t *testing.T) {
    t.Parallel()
    for _, tt := range pairs {
        WriteToMap(tt.k, tt.v)
    }
}
func TestReadFromMap(t *testing.T) {
    t.Parallel()
    for _, tt := range pairs {
        actual := ReadFromMap(tt.k)
        if actual != tt.v {
            t.Errorf("the value of key(%s) is %s, expected %s", tt.k, actual, tt.v)
        }
    }
}
试验步骤
注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码
同时注释掉测试代码中的 t.Parallel 执行测试 测试通过 即使加上 -race 测试依然通过
只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码 执行测试 测试失败 如果未失败 加上 -race 一定会失败
如果代码能够进行并行测试 在写测试时 尽量加上 Parallel 这样可以测试出一些可能的问题

Go 语言基本测试用例

测试文件的文件名 以_test.go 结尾 测试用例 以TestXxxx的样式
要测试utils包的sql.go中的函数
func GetOne(db *sql.DB, query string, args ...interface{}) (map[string][]byte, error) {
就需要创建一个sql_test.go
package utils
import (
    "database/sql"
    _ "fmt"
    _ "github.com/go-sql-driver/mysql"
    "strconv"
    "testing"
)
func Test_GetOne(t *testing.T) {
    db, err := sql.Open("mysql", "root:123.abc@tcp(192.168.33.10:3306)/test")
    defer func() {
        db.Close()
    }()
    if err != nil {
        t.Fatal(err)
    } // 测试empty
    car_brand, err := GetOne(db, "select * from user where id = 999999")
    if (car_brand != nil) || (err != nil) {
        t.Fatal("emtpy测试错误")
    }
}

Go 语言

testing 测试用例

测试用例有四种形式
TestXxxx(t testing.T)// 基本测试
BenchmarkXxxx(b testing.B)// 压力测试的测试
Example_Xxx()// 测试控制台输出的例子
TestMain(m *testing.M)// 测试Main函数
Example 在最后用注释的方式 确认控制台输出和预期是不是一致的
func Example_GetScore() {
    score := getScore(100, 100, 100, 2.1)
    fmt.Println(score)
    // Output:
    // 31.1
}

Go 语言 testing 变量

gotest的变量有
test.short 快速测试的标记 在测试用例中 使用testing.Short() 绕开一些测试
test.outputdir  输出目录
test.coverprofile  测试覆盖率参数 指定输出文件
test.run  指定正则来运行某个/某些测试用例
test.memprofile  内存分析参数 指定输出文件
test.memprofilerate  内存分析参数 内存分析的抽样率
test.cpuprofile  cpu  分析输出参数 为空则不做cpu分析
test.blockprofile   阻塞事件的分析参数 指定输出文件
test.blockprofilerate  阻塞事件的分析参数 指定抽样频率
test.timeout  超时时间
test.cpu  指定cpu数量
test.parallel  指定运行测试用例的并行数

testing包 结构

B  压力测试
BenchmarkResult  压力测试结果
Cover  代码覆盖率相关结构体
CoverBlock  代码覆盖率相关结构体
InternalBenchmark  内部使用的结构
InternalExample  内部使用的结构
InternalTest  内部使用的结构
M  main测试使用的结构
PB  Parallel benchmarks 并行测试使用结果
T  普通测试用例
TB  测试用例的接口

testing 通用方法

T结构内部 继承 common结构 common结构提供 集中方法  经常用到
当 遇到一个断言错误的时候  就会判断这个测试用例失败 就会使用到
Fail   case失败 测试用例继续
FailedNow  case失败 测试用例中断
当遇到一个断言错误 只希望跳过这个错误 但是不希望标示测试用例失败 会使用到
SkipNow  case跳过 测试用例不继续
当 只希望在一个地方打印出信息  会用到:
Log  输出信息
Logf  输出有format的信息
当 希望跳过这个用例 并且打印出信息:
Skip  Log + SkipNow
Skipf  Logf + SkipNow
当 希望断言失败的时候 测试用例失败 打印出必要的信息 但是测试用例继续
Error  Log + Fail
Errorf  Logf + Fail
当 希望断言失败的时候 测试用例失败 打印出必要的信息 测试用例中断
Fatal  Log + FailNow
Fatalf  Logf + FailNow
GO 中如何进行单元测试