二丫讲梵 二丫讲梵
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • AWS
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancher
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • LLM
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • json2go (opens new window)
    • gopher (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)

二丫讲梵

行者常至,为者常成
首页
  • 最佳实践
  • 迎刃而解
  • Nginx
  • Php
  • Zabbix
  • AWS
  • Prometheus
  • Grafana
  • CentOS
  • Systemd
  • Docker
  • Rancher
  • Ansible
  • Ldap
  • Gitlab
  • GitHub
  • Etcd
  • Consul
  • RabbitMQ
  • Kafka
  • MySql
  • MongoDB
  • OpenVPN
  • KVM
  • VMware
  • Other
  • ELK
  • K8S
  • LLM
  • Nexus
  • Jenkins
  • 随写编年
  • 家人物语
  • 追忆青春
  • 父亲的朋友圈
  • 电影音乐
  • 效率工具
  • 博客相关
  • Shell
  • 前端实践
  • Vue学习笔记
  • Golang学习笔记
  • Golang编程技巧
  • 学习周刊
  • Obsidian插件周刊
关于
友链
  • 本站索引

    • 分类
    • 标签
    • 归档
  • 本站页面

    • 导航
    • 打赏
  • 我的工具

    • 备忘录清单 (opens new window)
    • json2go (opens new window)
    • gopher (opens new window)
    • 微信MD编辑 (opens new window)
    • 国内镜像 (opens new window)
    • 出口IP查询 (opens new window)
    • 代码高亮工具 (opens new window)
  • 外站页面

    • 开往 (opens new window)
    • ldapdoc (opens new window)
    • HowToStartOpenSource (opens new window)
    • vdoing-template (opens new window)
GitHub (opens new window)
  • Shell编程

  • Go编程笔记

  • 前端编程笔记

  • Go学习笔记

    • 基础部分

      • 基础知识
      • 数据类型
      • fmt的格式化输入输出
      • 运算符
      • 流程控制
      • 认识函数
      • 包的操作
      • 项目工程-在同级目录
      • 指针(pointer)
      • 数组(array)
      • 切片(slice)
      • 字典(map)
      • 结构体(struct)
      • 匿名组合
      • 方法
      • 接口
      • error接口
      • panic
      • recover
      • 字符串操作
      • 字符串转换
      • 正则表达式
      • JSON处理
      • 文件的操作
      • goroutine
      • channel
      • select
      • sync-lock锁
      • 网络概述
      • Socket编程
      • 发文件与聊天室
      • http编程
      • 爬虫实战
      • 单元测试了解
        • 1,go test
        • 2,测试函数格式
        • 3,测试函数示例
        • 4,测试组
        • 5,子测试
        • 6,测试覆盖率
        • 7,基准测试函数
          • 1,基准测试示例
    • web框架

    • orm框架

  • Vue-21年学习笔记

  • Vue-22年重学笔记

  • 编程世界
  • Go学习笔记
  • 基础部分
二丫讲梵
2021-07-10
目录

单元测试了解

文章发布较早,内容可能过时,阅读注意甄别。

不写测试的开发不是好的程序猿。Go 语言中的测试依赖go test命令。编写测试代码和编写普通的 Go 代码过程是类似的,并不需要学习新的语法,规则或者工具。

# 1,go test

go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀的源代码文件都是测试的一部分,不会被go build编译到最终的可执行文件中。

在*_test.go文件中有三种类型的函数,单元测试函数,基准测试函数和示例函数。

类型 格式 作用
测试函数 函数名前缀为 Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为 Benchmark 测试函数的性能
示例函数 函数名前缀为 Example 为文档提供示例文档

go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的 main 包用于调用相应的测试函数,然后构建并运行,报告测试结果,最后清理测试中生成的临时文件。

# 2,测试函数格式

每个测试函数必须导入 testing 包,测试函数基本格式如下:

func TestName(t *testing.T){
		//...
}
1
2
3

测试函数的名字必须以 Test 开头,可选的后缀名必须以大写字母开头:

func TestAdd(t *testing.T){...}
func TestSum(t *testing.T){...}
1
2

其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:

func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 3,测试函数示例

模拟日常开发过程, 我们先写一个功能单一的程序,这个程序可以分割字符串,代码如下:

package split

import "strings"

// Split 切割一个字符串
func Split(str string, sep string) []string {
	var tmp []string
	index := strings.Index(str, sep)
	for index >= 0 {
		tmp = append(tmp, str[:index])
		str = str[index+1:]
		index = strings.Index(str, sep)
	}
	tmp = append(tmp, str)
	return tmp
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在同级目录下,可以创建一个 split_test.go的测试文件,并定义一个测试函数如下:

package split

import (
	"reflect"
	"testing"
)

// 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
func TestSplit(t *testing.T) {
	// 程序输出的结果
	got := Split("a:b:c", ":")
	// 期望的结果
	want := []string{"a", "b", "c"}
	// 因为slice不能比较直接,借助反射包中的方法比较
	if !reflect.DeepEqual(want, got) {
		// 测试失败输出错误提示
		t.Errorf("test failed, want:%v but got:%v\n", want, got)
	}
}

func Test2Split(t *testing.T) {
	got := Split("abcdef", "c")
	want := []string{"ab", "def"}
	if !reflect.DeepEqual(want, got) {
		t.Errorf("test failed, want:%v but got:%v\n", want, got)
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

然后在这个目录中,直接运行 go test可以看到输出结果:

$ go test
PASS
ok      oldboy/day09/split      0.005s
1
2
3

使用 go test -v可以看到详细内容:

$ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   Test2Split
--- PASS: Test2Split (0.00s)
PASS
ok      oldboy/day09/split      0.005s
1
2
3
4
5
6
7

如上测试用例当中写了两个测试用例,都验证通过了,现在再写一个如下内容的用例:

func Test3Split(t *testing.T) {
	got := Split("abcdef", "cd")
	want := []string{"ab", "ef"}
	if !reflect.DeepEqual(want, got) {
		t.Errorf("test failed, want:%v but got:%v\n", want, got)
	}
}
1
2
3
4
5
6
7

跑一下,发现报错了:

$ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
=== RUN   Test2Split
--- PASS: Test2Split (0.00s)
=== RUN   Test3Split
--- FAIL: Test3Split (0.00s)
    split_test.go:33: test failed, want:[ab ef] but got:[ab def]
FAIL
exit status 1
FAIL    oldboy/day09/split      0.004s
1
2
3
4
5
6
7
8
9
10
11

看起来程序采用这个用例就跑不过去了,那么就需要调整程序:

package split

import (
	"fmt"
	"strings"
)

// Split 切割一个字符串
func Split(str string, sep string) []string {
	var tmp []string
	index := strings.Index(str, sep)
	fmt.Println(index)
	for index >= 0 {
		tmp = append(tmp, str[:index])
		str = str[index+len(sep):]  // 这里使用len(sep)获取sep的长度
		index = strings.Index(str, sep)
		fmt.Println(index)
	}
	tmp = append(tmp, str)
	return tmp
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

再跑一下,就通过。

# 4,测试组

我们现在还想要测试一下split函数对中文字符串的支持,这个时候我们可以再编写一个TestChineseSplit测试函数,但是我们也可以使用如下更友好的一种方式来添加更多的测试用例。

package split

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) {
	// 定义一个测试用例类型
	type testCase struct {
		str  string
		sep  string
		want []string
	}
	// 定义一个存储测试用例的切片
	testGroup := []testCase{
		testCase{"a:b:c", ":", []string{"a", "b", "c"}},
		testCase{"abcdef", "c", []string{"ab", "def"}},
		testCase{"你好吗", "好", []string{"你", "吗"}},
	}
	// 遍历切片,逐一执行测试用例
	for _, tc := range testGroup {
		got := Split(tc.str, tc.sep)
		if !reflect.DeepEqual(tc.want, got) {
			t.Fatalf("test failed,want:%v,got:%v\n", tc.want, got)
		}
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

执行结果如下:

$ go test -v
=== RUN   TestSplit
--- PASS: TestSplit (0.00s)
PASS
ok      oldboy/day09/split      0.004s
1
2
3
4
5

# 5,子测试

上边的定义方式大大简化了针对同一功能代码用例的编写,只需要在切片中新增一个 testCase 即可,但是有一个不足就是所有用例执行结果不能够清晰逐个看到。在 Go1.7+中新增了子测试,可以通过定义 map 的方式,将每个用例名字打印出来:

package split

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) {
	// 定义一个测试用例类型
	type testCase struct {
		str  string
		sep  string
		want []string
	}
	// 定义一个存储测试用例的map
	testGroup := map[string]testCase{
		"case-1": testCase{"a:b:c", ":", []string{"a", "b", "c"}},
		"case-2": testCase{"abcdef", "c", []string{"ab", "def"}},
		"case-3": testCase{"你好吗", "好", []string{"你", "吗"}},
	}
	// 遍历map,逐一执行测试用例
	for name, tc := range testGroup {
		t.Run(name, func(t *testing.T) {
			got := Split(tc.str, tc.sep)
			if !reflect.DeepEqual(tc.want, got) {
				t.Fatalf("test failed,want:%#v,got:%#v\n", tc.want, got)
			}
		})

	}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

执行效果如下:

$ go test -v
=== RUN   TestSplit
=== RUN   TestSplit/case-3
=== RUN   TestSplit/case-1
=== RUN   TestSplit/case-2
--- PASS: TestSplit (0.00s)
    --- PASS: TestSplit/case-3 (0.00s)
    --- PASS: TestSplit/case-1 (0.00s)
    --- PASS: TestSplit/case-2 (0.00s)
PASS
ok      oldboy/day09/split      0.005s
1
2
3
4
5
6
7
8
9
10
11

如果想要指定执行其中某个用例,可用如下方式进行:

$ go test -v -run=TestSplit/case-2
=== RUN   TestSplit
=== RUN   TestSplit/case-2
--- PASS: TestSplit (0.00s)
    --- PASS: TestSplit/case-2 (0.00s)
PASS
ok      oldboy/day09/split      0.004s
1
2
3
4
5
6
7

# 6,测试覆盖率

测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。

Go 提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如:

$ go test -cover
PASS
coverage: 100.0% of statements
ok      oldboy/day09/split      0.004s
1
2
3
4

从上面的结果可以看到我们的测试用例覆盖了 100%的代码。

Go 还提供了一个额外的-coverprofile参数,用来将覆盖率相关的记录信息输出到一个文件。例如:

$ go test -cover -coverprofile=c.out
PASS
coverage: 100.0% of statements
ok      oldboy/day09/split      0.004s
1
2
3
4

上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中,然后我们执行go tool cover -html=c.out,使用cover工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个 HTML 报告。

image-20200219225154550

图中每个用绿色标记的语句块表示被覆盖了,而红色的表示没有被覆盖。

日常开发当中:

函数覆盖率:应该达到 100%

代码覆盖率:应该超过 60%

# 7,基准测试函数

基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:

func BenchmarkName(b *testing.B){
    // ...
}
1
2
3

基准测试以 Benchmark 为前缀,需要一个*testing.B 类型的参数 b,基准测试必须要执行 b.N 次,这样的测试才有对照性,b.N 的值是系统根据实际情况去调整的,从而保证测试的稳定性。 testing.B 拥有的方法如下:

func (c *B) Error(args ...interface{})
func (c *B) Errorf(format string, args ...interface{})
func (c *B) Fail()
func (c *B) FailNow()
func (c *B) Failed() bool
func (c *B) Fatal(args ...interface{})
func (c *B) Fatalf(format string, args ...interface{})
func (c *B) Log(args ...interface{})
func (c *B) Logf(format string, args ...interface{})
func (c *B) Name() string
func (b *B) ReportAllocs()
func (b *B) ResetTimer()
func (b *B) Run(name string, f func(b *B)) bool
func (b *B) RunParallel(body func(*PB))
func (b *B) SetBytes(n int64)
func (b *B) SetParallelism(p int)
func (c *B) Skip(args ...interface{})
func (c *B) SkipNow()
func (c *B) Skipf(format string, args ...interface{})
func (c *B) Skipped() bool
func (b *B) StartTimer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 1,基准测试示例

我们为 split 包中的Split函数编写基准测试如下:

$ cat split_test.go

package split

import (
	"testing"
)

func BenchmarkSplit(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Split("a:b:c:d:e", ":")
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:

$ go test -bench=Split
goos: darwin
goarch: amd64
pkg: oldboy/day09/split
BenchmarkSplit-4         5000000               242 ns/op
PASS
ok      oldboy/day09/split      1.473s
1
2
3
4
5
6
7

其中BenchmarkSplit-4表示对 Split 函数进行基准测试,数字4表示GOMAXPROCS的值,这个对于并发基准测试很重要。5000000和242ns/op表示每次调用Split函数耗时242ns,这个结果是5000000次调用的平均值。

我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。

$ go test -bench=Split -benchmem
goos: darwin
goarch: amd64
pkg: oldboy/day09/split
BenchmarkSplit-4         5000000               250 ns/op             240 B/op          4 allocs/op
PASS
ok      oldboy/day09/split      1.515s
1
2
3
4
5
6
7

其中,240 B/op表示每次操作内存分配了 240 字节,4 allocs/op则表示每次操作进行了 3 次内存分配。 我们将我们的Split函数优化如下:

// Split 切割一个字符串
func Split(str string, sep string) []string {
	var tmp = make([]string, 0, strings.Count(str, sep)+1)  // 在声明变量的时候直接make分配内存空间
	index := strings.Index(str, sep)
	for index >= 0 {
		tmp = append(tmp, str[:index])
		str = str[index+len(sep):]
		index = strings.Index(str, sep)
	}
	tmp = append(tmp, str)
	return tmp
}
1
2
3
4
5
6
7
8
9
10
11
12

改进之后再次进行性能测试看看效果:

$ go test -bench=Split -benchmem
goos: darwin
goarch: amd64
pkg: oldboy/day09/split
BenchmarkSplit-4        20000000               101 ns/op              80 B/op          1 allocs/op
PASS
ok      oldboy/day09/split      2.146s
1
2
3
4
5
6
7

这个使用 make 函数提前分配内存的改动,减少了 2/3 的内存分配次数,并且减少了一半的内存分配。

微信 支付宝
#go
上次更新: 2024/07/04, 22:40:37
爬虫实战
标准库template学习入门

← 爬虫实战 标准库template学习入门→

最近更新
01
记录二五年五一之短暂回归家庭
05-09
02
学习周刊-总第210期-2025年第19周
05-09
03
学习周刊-总第209期-2025年第18周
05-03
更多文章>
Theme by Vdoing | Copyright © 2017-2025 | 点击查看十年之约 | 浙ICP备18057030号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式