测试关系着系统的质量和稳定性。若系统出现严重漏洞或bug,则极有可能造成严重事故,因此 测试 是生产过程中必不可少的一部分。接下来简单介绍go中单元测试、mock测试、基准测试。
1. 单元测试
1.1 单元测试规则
- 测试文件:每个需要测试的包通常会有一个或多个以
_test.go结尾的文件,这些文件包含了该包的所有测试代码。 - 测试函数:测试函数必须以
Test开头,并接受一个类型为*testing.T的参数。这个参数提供了报告失败、记录日志等功能的方法。 - 初始化逻辑放到Testmain()函数中。
- 测试运行:运行测试,只需在
包含测试文件的目录下执行go test命令即可:
1.2 单元测试assert断言验证
- testify/assert的断言方法: 首先要安装第三方库:go get github.com/stretchr/testify
- Equal:检查两个值是否相等。
assert.Equal(t, expected, actual, "message") - NotEqual:检查两个值是否不相等。
assert.NotEqual(t, expected, actual, "message") - Nil:检查一个值是否为
nil。assert.Nil(t, value, "message") - NotNil:检查一个值是否不为
nil。assert.NotNil(t, value, "message") - True:检查一个布尔表达式是否为
true。assert.True(t, condition, "message") - False:检查一个布尔表达式是否为
false。assert.False(t, condition, "message") - Panics:检查某段代码是否会引发panic。
assert.Panics(t, func(), "message") - NoError:检查一个函数返回的错误是否为
nil。result, err := SomeFunction() assert.NoError(t, err, "message")
1.3 单元测试_覆盖率
为了了解测试对代码的覆盖程度,可以使用-cover标志:
go test -cover
这将显示测试覆盖了多少代码。还可以生成详细的HTML报告:
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
1.4 单元测试_mock
Mock 是一种特殊的测试辅助对象,用于模拟被测试代码的依赖项。这些依赖项可以是数据库、网络服务、文件系统或其他任何外部系统。 Mock 对象的主要作用是:
- 隔离依赖:确保测试只关注当前代码的功能,而不受外部系统的影响。
- 控制行为:模拟依赖项的行为,以便在测试中精确控制输入和输出。
- 验证交互:检查被测试代码是否正确地调用了依赖项的方法,以及调用的次数和参数
- 在单元测试中,我们希望尽可能地隔离被测试代码与外部依赖。直接修改代码中的函数调用会改变生产代码的行为,这不仅会影响其他测试,还可能引入潜在的bug。因此使用打桩mock就不必依赖于实际的文件I/O操作,这样允许在测试环境中完全控制函数的行为,而无需处理真实的文件系统。
- 对readfirstline进行打桩,不在依赖本地文件
2 基准测试Benchmarking)
是评估代码性能的一种重要手段
2.1 基本概念
- 基准测试函数:基准测试函数必须以
Benchmark开头,并接受一个类型为*testing.B的参数。这个参数提供了控制测试循环和记录结果的方法。 - 测试循环:基准测试通常在一个循环中多次执行被测试的代码,以确保结果的准确性和可重复性。
- 性能指标:基准测试的结果通常以纳秒(ns)为单位表示每次操作的平均执行时间,也可以表示每秒的操作次数(ops/s)。
2.2 基准测试规则
- 基准测试的运行:要运行基准测试,可以使用
go test命令,并加上-bench标志:
go test -bench=.
- 基准测试的输出 基准测试的输出通常包括以下几部分:
-
- 测试名称:基准测试函数的名称。
-
- 迭代次数:基准测试中执行的迭代次数。
-
- 每次操作的时间:以纳秒为单位表示每次操作的平均执行时间。
-
- 每秒的操作次数:每秒可以执行的操作次数。
BenchmarkAdd-8 1000000000 0.30 ns/op
PASS
ok your/package/name 0.566s
在这个例子中:
-
BenchmarkAdd-8表示基准测试函数的名称,-8表示测试是在8个CPU核心上运行的。
-
1000000000表示测试中执行了10亿次迭代。
-
0.30 ns/op表示每次调用Add函数的平均时间为0.30纳秒。
-
PASS表示测试通过。
-
0.566s表示整个基准测试的总运行时间。
- 使用
b.ResetTimer:有时在基准测试中需要做一些准备工作,这些准备工作不应该计入测试时间。可以使用b.ResetTimer和b.StopTimer来控制计时器。
func BenchmarkAddWithPreparation(b *testing.B) {
// 做一些准备工作
a := 1
b := 2
// 重置计时器,开始计时
b.ResetTimer()
for i := 0; i < b.N; i++ {
Add(a, b)
}
}
- 使用
b.StopTimer:如果想测量内存分配情况,可以使用b.ReportAllocs。
func BenchmarkAddWithAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
3. 性能优化指南
go语言提供了支持基准测试的benchmark工具
此命令: go test -bench= .benchmem
3.1 slice预分配内存
在使用mack()进行初始化时候尽可能提供容量信息 切片本质是一个数组片段的描述
- 数组指针
- 片段长度
- 片段容量(不改变内存分配下的最大长度)
- 注意:切片操作不合复制切片指向的的元素。且创建新的切片会复制原来切片的底层数组
- 若是容量不足的话,底层数组会进行扩容操作,这会导致性能降低,因此为了避免内存拷贝的过程,若可以,则在初始化的时候设置号容量
- 大内存未得到释放:用 copy 代替 re-slice
- 在已有切片的基础上,不会创建新的底层数组。因此会出现一个问题:原切片比较大,而想在原切片的基础上新建小切片,那么原底层数组在内存中有引用,则内存也会被引入,得不到释放。
3.2 map预分配内存
- 不断在map中添加元素的操作会触发map扩容操作
- 提前分配内存可减少内存拷贝和rehash的消耗
3.3 字符串处理
-
- 字符串在Go中是不可变类型,占用内存大小是固定的
-
- 每次使用 + 进行拼接都会重新分配内存
-
- string.Builder 和 byte.Buffer 底层都是 []byte数组
-
- 内存扩容策略不需要每次拼接重新分配内存
- 使用 string.Builder
分析 string.Builder 和 byte.Buffer
- string.Builder直接将底层[]byte数组转化为字符串类型返回
- byte.Buffer转化成字符串时候重新申请了一块空间
3.4 空结构体
- 空结构体实例不占据任何内存空间
- 可以作为占位符使用以节省资源
在此场景中,只需要用到map的键,而值bool会多占据一个字节空间
3.5 atomic包
- 锁的实现是通过操作系统来实现的,属于系统调用
- atomic 操作是通过硬件来实现的,效率比锁高
- sync.Mutex 保护一层逻辑,而不仅仅保护一个变量
- 对于非数值操作,可以用 atomic.Value,可承载一个interface{}