初见Go-Go测试

go test 命令

go test命令是一个按照一定的约定和组织来测试代码的程序。在包目录内,所有以_test.go为后缀名的源文件在执行go build时不会被构建成包的一部分,它们是go test测试的一部分。

*_test.go文件中,有三种类型的函数:测试函数基准测试(benchmark)函数、示例函数

  • 测试函数是以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test命令会调用这些测试函数并报告测试结果是PASS或FAIL。
  • 基准测试函数是以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能;go test命令会多次运行基准测试函数以计算一个平均的执行时间。
  • 示例函数是以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。

go test 可以使用额外参数打印更多的信息,不过大部分IDE集成了这部分功能

参数名 功能
-v 打印每个测试函数的名字和运行时间
-run -run = xxx对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被go test测试命令运行
-cover 获得测试的代码覆盖率摘要
-bench -bench = xxx 指定要运行的基准测试函数

测试函数

  • 每个测试函数必须导入testing包。
  • 测试函数名必须以 Test 开头
  • *testing.T 类型参数用于报告测试失败和附加的日志信息
func TestName(t *testing.T) {
    // ...
}

表格驱动的测试

为了方便添加测试数据,我们可以将测试数据合并到一个表格之中,比如一个结构体数组中,这种测试方式称之为表格驱动的测试

func TestBruteForce(t *testing.T) {
	var tests = []struct {
		s       string  // 主串
		pattern string  // 模式串
		want    int     // 主串中匹配到的模式串的起始下标
	}{
		{"abcd", "bcd", 1},
		{"abcd", "e", -1},
		{"abcd", "", 0},
	}

	for _, test := range tests {
		if got := bruteForce(test.s, test.pattern); got != test.want {
			t.Errorf("bruteForce(%q, %q) = %v", test.s, test.pattern, got)
		}
	}
}

上面这段代码,测试一个字符串匹配函数。如果有新的测试样例,就可以直接加到表格的下一行中,非常方便。

随机测试

除了构造精心设计的测试样例,有时候我们想通过构造更广泛的随即输入来测试函数的行为,这种测试思路为随机测试

func TestRandomBruteForce(t *testing.T) {
	seed := time.Now().UTC().UnixNano()
	rng := rand.New(rand.NewSource(seed))
	t.Logf("Random seed: %d", seed) //在日志中记录随机数种子
	for i := 0; i < 1000; i++ {
		s, pattern, start := randomString(rng)
		if got := bruteForce(s, pattern); got != start {
			t.Errorf("bruteForce(%q, %q) = %v", s, pattern, got)
		}

	}
}

// 随机生成主串和子串
func randomString(rng *rand.Rand) (string, string, int) {
	n := rng.Intn(25) // 随机字符串长度
	bytes := make([]byte, n)
	for i := 0; i < (n+1)/2; i++ {
		r := rune(rng.Intn('z'-'a'+1) + 'a') // 只包含小写字母
		bytes[i] = byte(r)
		bytes[n-1-i] = byte(r)
	}
	// 随机生成字符串的子串
	length := len(bytes)
	if length == 0 {
		return "", "", 0
	}
	start := 0
	end := rng.Intn(length-start+1) + start
	subBytes := bytes[start:end]
	return string(bytes), string(subBytes), start
}

随机测试生成的大量数据,没必要全部保存下来,只需要记录每次测试时的随机种子,如果测试出现问题,只需要那出现问题的那次随机种子来重新测试获得失败样例。


测试语句覆盖率

测试覆盖率通常被用来衡量测试的充分性和完整性

语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例

执行go test xx_test.go xx.go -cover获得 xx.go 的测试文件代码覆盖率摘要。

ok      command-line-arguments  0.364s  coverage: 92.9% of statements

Mock测试

作用

让测试函数不依赖本地环境。比如一个函数通过请求远程库来获取数据,但是这时候远程库还未实现。又或者一个函数通过读取本地文件来修改,如果文件变动就会影响测试结果。

bou.ke/monkey 库简单介绍

monkey 是一个Go单元测试中十分常用的打桩工具,它在运行时通过汇编语言重写可执行文件,将目标函数或方法的实现跳转到桩实现,其原理类似于热补丁。

另外使用时要注意

  • monkey不支持内联函数,在测试的时候需要通过命令行参数-gcflags=-l关闭Go语言的内联优化,否则可能无法成功。
  • monkey不是线程安全的,所以不要把它用到并发的单元测试中。

Patch / Unpatch 方法

  • Patch 接收两个参数 target 和 replacement

target 可以是函数或方法,replacement 是打桩函数。

// Patch replaces a function with another
func Patch(target, replacement interface{}) *PatchGuard {
   t := reflect.ValueOf(target)
   r := reflect.ValueOf(replacement)
   patchValue(t, r)

   return &PatchGuard{t, r}
}
  • Unpatch 用于在测试结束后卸载桩
// Unpatch removes any monkey patches on target
// returns whether target was patched in the first place
func Unpatch(target interface{}) bool {
   return unpatchValue(reflect.ValueOf(target))
}

简单使用

  1. 安装mock 库 monkey

    go get bou.ke/monkey
    
  2. 编写如下测试代码

    import (
    	"fmt"
    	"testing"
    
    	"bou.ke/monkey"
    )
    
    func A() string {
    	return "A"
    }
    func TestA(t *testing.T) {
    	monkey.Patch(A, func() string {
    		return "B"
    	})
    	defer monkey.Unpatch(A())
    	fmt.Println(A()) // "B"
    }
    
  3. 在控制台执行 go test -gcflags=-l ,输出"B" ,函数A() 被替换掉了


基准测试

基准测试是测量一个程序在固定工作负载下的性能。和普通测试不同的是,默认情况下不运行任何基准测试。我们需要通过-bench命令行标志参数手工指定要运行的基准测试函数。

  • 每个测试函数必须导入testing包。
  • 基准测试函数名必须以 Benchmark 开头
  • *testing.B 类型参数除了提供和*testing.T类似的方法,还有额外一些和性能测量相关的方法。它还提供了一个整数N,用于指定操作执行的循环次数。
func BenchmarkBruteForce(b *testing.B) {
	for i := 0; i < b.N; i++ {
		bruteForce("hello world", "world")
	}
}

在Goland中直接运行测试测试结果如下

goos: windows
goarch: amd64
pkg: MyUtils/stringMatch/bruteForce
cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz
BenchmarkBruteForce
BenchmarkBruteForce-8           53356231                22.45 ns/op
PASS

示例函数

  • 以Example为函数名开头
  • 示例函数没有函数参数和返回值
  • 作为示例函数的函数必须以大写字母开头

下面为A函数的示例函数

func A() {
   // ...
}
func ExampleA() {
   // ...
}

示例函数作用:

  1. 函数的注释会引用示例函数

  2. go test执行测试的时候也会运行示例函数测试。如果示例函数内含有// Output:格式的注释,那么测试工具会执行这个示例函数,然后检查示例函数的标准输出与注释是否匹配。


参考资料

《go 语言圣经中文版》
《Go单测从零到溜系列4—使用monkey打桩》
《Go语言中的内联函数》

posted @ 2022-05-08 17:06  油虾条  阅读(77)  评论(0编辑  收藏  举报