Go语言测试 | 青训营笔记

37 阅读4分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 3 天。

今天就之前没有完成的Go语言测试实践部分进行完善。主要参考第九章 测试 · Go语言标准库 (studygolang.com),进行学习。

单元测试

最基础的测试就不做赘述了,仅记录一下相关代码。

待测试代码:

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)
	}
}

func TestFib2(t *testing.T) {
	var fibTests = []struct {
		in       int // input
		expected int // expected result
	}{
		{1, 1},
		{2, 1},
		{3, 2},
		{4, 3},
		{5, 5},
		{6, 8},
		{7, 13},
	}

	for _, tt := range fibTests {
		actual := Fib(tt.in)
		if actual != tt.expected {
			t.Errorf("Fib(%d) = %d; expected %d", tt.in, actual, tt.expected)
		}
	}
}

执行命令进行测试:

go test .

并行测试

演示代码,简单来说就是对map进行读写操作:

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", " 徐新华 "},
    {"studygolang", "Go 语言中文网 "},
    {"stdlib", "Go 语言标准库 "},
    {"polaris1", " 徐新华 1"},
    {"studygolang1", "Go 语言中文网 1"},
    {"stdlib1", "Go 语言标准库 1"},
    {"polaris2", " 徐新华 2"},
    {"studygolang2", "Go 语言中文网 2"},
    {"stdlib2", "Go 语言标准库 2"},
    {"polaris3", " 徐新华 3"},
    {"studygolang3", "Go 语言中文网 3"},
    {"stdlib3", "Go 语言标准库 3"},
    {"polaris4", " 徐新华 4"},
    {"studygolang4", "Go 语言中文网 4"},
    {"stdlib4", "Go 语言标准库 4"},
}

// 注意 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)
        }
    }
}

试验步骤:

  1. 注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,同时注释掉测试代码中的 t.Parallel,执行测试,测试通过,即使加上 -race,测试依然通过;
  2. 只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,执行测试,测试失败(如果未失败,加上 -race 一定会失败);

如果代码能够进行并行测试,在写测试时,尽量加上 Parallel,这样可以测试出一些可能的问题。

并行测试问题

首先 ,-race参数要求必须将环境变量CGO_ENABLED=1才可以生效,如何设置这里不做详细介绍。

禁用缓存

本文go version为1.18,go test时会存在缓存问题,毕竟并行的对map进行读写操作不一定百分之百一定出错的,因此要禁用缓存。具体见:go test 禁用测试缓存 - 知乎 (zhihu.com),结论如下:

有以下三种方式, 在测试中禁用缓存:

  1. 执行 go test添加 --count=1 参数(推荐,效率高),以上面 例子:
CGO_ENABLED=1 go test -v --count=1 --mod=vendor ./pkg/...
  1. Go 官方提供 clean工具,来删除对象文件和缓存文件, 不过这种方式相对麻烦:
go clean -testcache // Delete all cached test results
  1. 设置 GOCACHE 环境变量。GOCACHE 指定了 go 命令执行时缓存的路径,以便之后被复用。 设置 GOCACHE=off 即可禁用缓存。

实验预期不符

以上实验步骤,如果仅仅注释掉WriteToMap 和 ReadFromMap 中 locker 保护的代码,实验结果与预期不符。

  1. 如果不加上-race参数,则会有三种结果,一种是成功,一种是读写冲突导致失败fatal error: concurrent map read and map write,还有一种是读取到的值是空的,与原值不等。关于第三种情况,其实TestReadFromMap中只判断actual != tt.v 这一个条件是不够的,因为读写是并行的,可能值还没来得及写进去。判断条件应该改成actual != tt.v && actual != ""才符合预期。
  2. 加上-race参数且判断条件修改正确,则有两种结果,第一种是成功,第二种是失败race detected during execution of test。虽然成功的很少,但是不符合原文中一定会失败的描述。这里原因在于不加锁虽然会存在冲突,但还是存在一定几率正常的,加锁一定正常,不加锁未必一定异常。

代码覆盖率

记录一下如何统计代码覆盖率。

其中还可以将覆盖率统计存成文件,并用浏览器查看具体情况,参考golang 单元测试覆盖率 - 翔云123456 - 博客园 (cnblogs.com)

# covermode 有三个可选参数 "set", "count", or "atomic"
# set 语句是否执行,默认模式
# count 每个语句执行次数
# atomic 类似于count,表示并发程序中的精确技术
go test -covermode atomic .

基准测试

这部分比较简单,按照教程即可完成。

如测试代码:

func BenchmarkFib1(b *testing.B)  { benchmarkFib(1, b) }
func BenchmarkFib2(b *testing.B)  { benchmarkFib(2, b) }
func BenchmarkFib3(b *testing.B)  { benchmarkFib(3, b) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(10, b) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(20, b) }
func BenchmarkFib40(b *testing.B) { benchmarkFib(40, b) }

func benchmarkFib(i int, b *testing.B) {
	for n := 0; n < b.N; n++ {
		Fib(i)
	}
}

测试命令:

go test -bench .

Mock

这部分其实个人最感兴趣,但是今天时间不充分了,放到下一篇吧。