这是我参与「第五届青训营」伴学笔记创作活动的第 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)
}
}
}
试验步骤:
- 注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,同时注释掉测试代码中的 t.Parallel,执行测试,测试通过,即使加上
-race
,测试依然通过; - 只注释掉 WriteToMap 和 ReadFromMap 中 locker 保护的代码,执行测试,测试失败(如果未失败,加上
-race
一定会失败);
如果代码能够进行并行测试,在写测试时,尽量加上 Parallel,这样可以测试出一些可能的问题。
并行测试问题
首先 ,-race
参数要求必须将环境变量CGO_ENABLED=1
才可以生效,如何设置这里不做详细介绍。
禁用缓存
本文go version为1.18,go test时会存在缓存问题,毕竟并行的对map进行读写操作不一定百分之百一定出错的,因此要禁用缓存。具体见:go test 禁用测试缓存 - 知乎 (zhihu.com),结论如下:
有以下三种方式, 在测试中禁用缓存:
- 执行
go test
添加--count=1
参数(推荐,效率高),以上面 例子:
CGO_ENABLED=1 go test -v --count=1 --mod=vendor ./pkg/...
- Go 官方提供 clean工具,来删除对象文件和缓存文件, 不过这种方式相对麻烦:
go clean -testcache // Delete all cached test results
- 设置 GOCACHE 环境变量。GOCACHE 指定了 go 命令执行时缓存的路径,以便之后被复用。 设置
GOCACHE=off
即可禁用缓存。
实验预期不符
以上实验步骤,如果仅仅注释掉WriteToMap 和 ReadFromMap 中 locker 保护的代码,实验结果与预期不符。
- 如果不加上
-race
参数,则会有三种结果,一种是成功,一种是读写冲突导致失败fatal error: concurrent map read and map write
,还有一种是读取到的值是空的,与原值不等。关于第三种情况,其实TestReadFromMap
中只判断actual != tt.v
这一个条件是不够的,因为读写是并行的,可能值还没来得及写进去。判断条件应该改成actual != tt.v && actual != ""
才符合预期。 - 加上
-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
这部分其实个人最感兴趣,但是今天时间不充分了,放到下一篇吧。