单元测试

什么是单元& 单元测试

  • 单元是应用的最小可测试部件,如函数和对象的方法
  • 单元测试是软件开发中对最小单位进行正确性检验的测试工作

为什么进行单元测试

  • 保证 变更/重构的正确性,特别是在一些频繁变动和多人合作开发的项目中
  • 简化调试过程: 可以轻松的让我们知道哪一部分代码出了问题
  • 单测最好的文档:在单测中直接给出具体接口的使用方法,是最好的实例代码

单元测试用例编写的原则

  • 单一原则:一个测试用例只负责一个场景
  • 原子性:结果只有两种情况:Pass /Fail
  • 优先要核心组件和逻辑的测试用例
  • 高频使用库,util,重点覆盖这些

go 语言原生支持了单元测试,使用上非常简单

单测用例约定

  • 文件名必须要xx_test.go命名
  • 测试方法必须是 TestXXX开头
  • 方法中的参数 必须是 t *testing.T
  • 测试文件和被测试文件必须在一个包中

golang 的测试框架

testing

简单使用

  • 准备待测代码 compute.go
  1. package main
  2. func Add(a, b int) int {
  3. return a + b
  4. }
  5. func Mul(a, b int) int {
  6. return a * b
  7. }
  8. func Div(a, b int) int {
  9. return a / b
  10. }

准备测试用例 compute_test.go

  1. package main
  2. import "testing"
  3. func TestAdd(t *testing.T) {
  4. a := 10
  5. b := 20
  6. want := 30
  7. actual := Add(a, b)
  8. if want != actual {
  9. t.Errorf("[Add 函数参数 :%d %d][期望:%d][实际:%d]", a, b, want, actual)
  10. }
  11. }
  12. func TestMul(t *testing.T) {
  13. a := 10
  14. b := 20
  15. want := 300
  16. actual := Mul(a, b)
  17. if want != actual {
  18. t.Errorf("[Mul 函数参数 :%d %d][期望:%d][实际:%d]", a, b, want, actual)
  19. }
  20. }
  21. func TestDiv(t *testing.T) {
  22. a := 10
  23. b := 20
  24. want := 2
  25. actual := Div(a, b)
  26. if want != actual {
  27. t.Errorf("[Div 函数参数 :%d %d][期望:%d][实际:%d]", a, b, want, actual)
  28. }
  29. }

执行test go test -v .

  1. D:\nyy_work\go_path\src\pkg007>go test -v .
  2. === RUN TestAdd
  3. --- PASS: TestAdd (0.00s)
  4. === RUN TestMul
  5. compute_test.go:23: [Mul 函数参数 :10 20][期望:300][实际:200]
  6. --- FAIL: TestMul (0.00s)
  7. === RUN TestDiv
  8. compute_test.go:35: [Div 函数参数 :10 20][期望:2][实际:0]
  9. --- FAIL: TestDiv (0.00s)
  10. FAIL
  11. FAIL pkg007 0.664s
  12. FAIL

只执行某个函数 go test -run=TestAdd -v .

正则过滤函数名 go test -run=TestM.* -v .

-cover测试覆盖率 go test -v -cover

  • 用于统计目标包有百分之多少的代码参与了单测
  1. === RUN TestAdd
  2. --- PASS: TestAdd (0.00s)
  3. === RUN TestMul
  4. compute_test.go:23: [Mul 函数参数 :10 20][期望:300][实际:200]
  5. --- FAIL: TestMul (0.00s)
  6. === RUN TestDiv
  7. compute_test.go:35: [Div 函数参数 :10 20][期望:2][实际:0]
  8. --- FAIL: TestDiv (0.00s)
  9. FAIL
  10. coverage: 100.0% of statements
  11. exit status 1
  12. FAIL pkg007 0.694s
  13. D:\nyy_work\go_path\src\pkg007>go test -v -cover

子测试 t.run

  1. package main
  2. import "testing"
  3. func TestMul(t *testing.T) {
  4. t.Run("正数", func(t *testing.T) {
  5. if Mul(4, 5) != 20 {
  6. t.Fatal("muli.zhengshu.error")
  7. }
  8. })
  9. t.Run("负数", func(t *testing.T) {
  10. if Mul(2, -3) != -6 {
  11. t.Fatal("muli.fusshu.error")
  12. }
  13. })
  14. }
  • go test -v .
  1. === RUN TestMul
  2. === RUN TestMul/正数
  3. === RUN TestMul/负数
  4. --- PASS: TestMul (0.00s)
  5. --- PASS: TestMul/正数 (0.00s)
  6. --- PASS: TestMul/负数 (0.00s)
  7. PASS
  8. ok pkg007 0.701s
  • 指定func/sub 跑子测试
  1. go test -run=TestMul/正数 -v

table-driven tests

  • 所有用例的数据组织在切片 cases 中,看起来就像一张表,借助循环创建子测试。这样写的好处有:
    • 新增用例非常简单,只需给 cases 新增一条测试数据即可。
    • 测试代码可读性好,直观地能够看到每个子测试的参数和期待的返回值。
    • 用例失败时,报错信息的格式比较统一,测试报告易于阅读。
    • 如果数据量较大,或是一些二进制数据,推荐使用相对路径从文件中读取。
  • 举例 D:\go_path\pkg\mod\github.com\prometheus\prometheus@v1.8.2-0.20210321183757-31a518faab18\web\api\v1\api_test.go

GoConvey 测试框架

  • 为何使用它,if else 高度封装

安装 go get github.com/smartystreets/goconvey

使用

  • 准备业务代码 student.go
  1. package main
  2. import "fmt"
  3. type Student struct {
  4. Name string
  5. ChiScore int
  6. EngScore int
  7. MathScore int
  8. }
  9. func NewStudent(name string) (*Student, error) {
  10. if name == "" {
  11. return nil, fmt.Errorf("name为空")
  12. }
  13. return &Student{
  14. Name: name,
  15. }, nil
  16. }
  17. func (s *Student) GetAvgScore() (int, error) {
  18. score := s.ChiScore + s.EngScore + s.MathScore
  19. if score == 0 {
  20. return 0, fmt.Errorf("全都是0分")
  21. }
  22. return score / 3, nil
  23. }
  • 准备测试用例 student_test.go
  1. package main
  2. import (
  3. "testing"
  4. . "github.com/smartystreets/goconvey/convey"
  5. )
  6. func TestNewStudent(t *testing.T) {
  7. Convey("start test new", t, func() {
  8. stu, err := NewStudent("")
  9. Convey("空的name初始化错误", func() {
  10. So(err, ShouldBeError)
  11. })
  12. Convey("stu对象为nil", func() {
  13. So(stu, ShouldBeNil)
  14. })
  15. })
  16. }
  17. func TestScore(t *testing.T) {
  18. stu, _ := NewStudent("小乙")
  19. Convey("不设置分数可能出错", t, func() {
  20. sc, err := stu.GetAvgScore()
  21. Convey("获取分数出错了", func() {
  22. So(err, ShouldBeError)
  23. })
  24. Convey("分数为0", func() {
  25. So(sc, ShouldEqual, 0)
  26. })
  27. })
  28. Convey("正常情况", t, func() {
  29. stu.ChiScore = 60
  30. stu.EngScore = 70
  31. stu.MathScore = 80
  32. score, err := stu.GetAvgScore()
  33. Convey("获取分数出错了", func() {
  34. So(err, ShouldBeNil)
  35. })
  36. Convey("平均分大于60", func() {
  37. So(score, ShouldBeGreaterThan, 60)
  38. })
  39. })
  40. }
  • 执行 go test -v .
  1. === RUN TestNewStudent
  2. start test new
  3. 空的name初始化错误 .
  4. stu对象为nil .
  5. 2 total assertions
  6. --- PASS: TestNewStudent (0.00s)
  7. === RUN TestScore
  8. 不设置分数可能出错
  9. 获取分数出错了 .
  10. 分数为0 .
  11. 4 total assertions
  12. 正常情况
  13. 获取分数出错了 .
  14. 平均分大于60 .
  15. 6 total assertions
  16. --- PASS: TestScore (0.00s)
  17. PASS
  18. ok pkg007 (cached)

图形化使用

  1. 确保本地有goconvey的二进制
  • go get github.com/smartystreets/goconvey 我使用的是这个
  • go get github.com/smartystreets/goconvey/convey
  • 会将对应的二进制放到 $GOPATH/bin 下面
  1. 编辑环境变量把 $GOPATH/bin 加入PATH里面 或者写全路径
  2. 到测试的目录下,执行goconvey ,启动http 8000,自动跑测试用例
  3. 浏览器方位 127.0.0.1:8000

单元测试和基准测试 - 图1

testify

安装 github.com/stretchr/testify/assert

使用

  • cal.go
  1. package main
  2. func Add(x int ) (result int) {
  3. result = x +2
  4. return result
  5. }
  • cal_test.go
  1. package main
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/assert"
  5. )
  6. func TestAdd(t *testing.T) {
  7. // assert equality
  8. assert.Equal(t, Add(5), 7, "they should be equal")
  9. }

表驱动测试

  1. package main
  2. import (
  3. "testing"
  4. "github.com/stretchr/testify/assert"
  5. )
  6. func TestAdd(t *testing.T) {
  7. // assert equality
  8. assert.Equal(t, Add(5), 7, "they should be equal")
  9. }
  10. func TestCal(t *testing.T) {
  11. ass := assert.New(t)
  12. var tests = []struct {
  13. input int
  14. expected int
  15. }{
  16. {2, 4},
  17. {-1, 1},
  18. {0, 2},
  19. {-5, -3},
  20. {999999997, 999999999},
  21. }
  22. for _, test := range tests {
  23. ass.Equal(Add(test.input), test.expected)
  24. }
  25. }

mock功能

单元测试覆盖率应用实例

基准测试

基准测试目的

  • 检测代码中方法性能问题

用法

  • fib.go
  1. package main
  2. func fib(n int) int {
  3. if n == 0 || n == 1 {
  4. return n
  5. }
  6. return fib(n-2) + fib(n-1)
  7. }
  • fib_test.go
  1. package main
  2. import "testing"
  3. func BenchmarkFib(b *testing.B) {
  4. for n := 0; n < b.N; n++ {
  5. fib(30)
  6. }
  7. }
  • go test -bench=. -run=none
  • go test 会在运行基准测试之前之前执行包里所有的单元测试,所有如果你的包里有很多单元测试,或者它们会运行很长时间,你也可以通过 go test 的-run 标识排除这些单元测试
  1. goos: windows
  2. goarch: amd64
  3. pkg: pkg007
  4. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  5. BenchmarkFib-12 182 6451155 ns/op
  6. PASS
  7. ok pkg007 2.532s

打印内存消耗情况 go test -bench=. -benchmem -run=none

  1. D:\nyy_work\go_path\src\pkg007>go test -bench=. -benchmem -run=none
  2. goos: windows
  3. goarch: amd64
  4. pkg: pkg007
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkFib-12 183 6272054 ns/op 0 B/op 0 allocs/op
  7. PASS
  8. ok pkg007 2.452s

bench的工作原理

  • 基准测试函数会被一直调用直到b.N无效,它是基准测试循环的次数
  • b.N 从 1 开始,如果基准测试函数在1秒内就完成 (默认值),则 b.N 增加,并再次运行基准测试函数。
  • b.N 的值会按照序列 1,2,5,10,20,50,… 增加,同时再次运行基准测测试函数。
  • 上述结果解读代表 1秒内运行了168次 每次 6566836 ns
  • -8 后缀和用于运行次测试的 GOMAXPROCS 值有关。 与GOMAXPROCS一样,此数字默认为启动时Go

传入cpu num进行测试

  • go test -bench=. -cpu=1,2,4 -benchmem -run=none
  1. D:\nyy_work\go_path\src\pkg007>go test -bench=. -cpu=1,2,4 -benchmem -run=none
  2. goos: windows
  3. goarch: amd64
  4. pkg: pkg007
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkFib 182 6238191 ns/op 0 B/op 0 allocs/op
  7. BenchmarkFib-2 188 6203264 ns/op 0 B/op 0 allocs/op
  8. BenchmarkFib-4 189 6360078 ns/op 0 B/op 0 allocs/op
  9. PASS
  10. ok pkg007 6.245s

- count 多次运行基准测试

  • 因为热缩放、内存局部性、后台处理、gc活动等等会导致单次的误差
  • go test -bench=. -count=10 -benchmem -run=none
  1. goos: windows
  2. goarch: amd64
  3. pkg: pkg007
  4. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  5. BenchmarkFib-12 184 6320565 ns/op 0 B/op 0 allocs/op
  6. BenchmarkFib-12 187 6229882 ns/op 0 B/op 0 allocs/op
  7. BenchmarkFib-12 186 6288768 ns/op 0 B/op 0 allocs/op
  8. BenchmarkFib-12 190 6274037 ns/op 0 B/op 0 allocs/op
  9. BenchmarkFib-12 188 6224385 ns/op 0 B/op 0 allocs/op
  10. BenchmarkFib-12 192 6468373 ns/op 0 B/op 0 allocs/op
  11. BenchmarkFib-12 175 6715403 ns/op 0 B/op 0 allocs/op
  12. BenchmarkFib-12 193 6404352 ns/op 0 B/op 0 allocs/op
  13. BenchmarkFib-12 181 7139169 ns/op 0 B/op 0 allocs/op
  14. BenchmarkFib-12 165 6799398 ns/op 0 B/op 0 allocs/op
  15. PASS
  16. ok pkg007 19.167s

-benchtime 指定运行秒数

  • 有的函数比较慢,为了更精确的结果,我们可以通过 -benchtime 标志指定运行时间,从而使它运行更多次。
  • go test -bench=. -benchtime=5s -benchmem -run=none
  1. goos: windows
  2. goarch: amd64
  3. pkg: pkg007
  4. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  5. BenchmarkFib-12 985 6400913 ns/op 0 B/op 0 allocs/op
  6. PASS
  7. ok pkg007 16.568s

ResetTimer

  • 如果基准测试在循环前需要一些耗时的配置,则可以先重置定时器
  1. package main
  2. import (
  3. "testing"
  4. "time"
  5. )
  6. func BenchmarkFib(b *testing.B) {
  7. time.Sleep(3 * time.Second)
  8. b.ResetTimer()
  9. for n := 0; n < b.N; n++ {
  10. fib(30)
  11. }
  12. }

-benchmem 展示内存消耗情况

  • go test -bench=. -benchtime=5s -benchmem -run=none
  • 测试大cap的切片,直接用cap初始化vs 动态扩容
  1. package main
  2. import (
  3. "math/rand"
  4. "testing"
  5. "time"
  6. )
  7. // 制定大的cap的切片
  8. func generateWithCap(n int) []int {
  9. rand.Seed(time.Now().UnixNano())
  10. nums := make([]int, 0, n)
  11. for i := 0; i < n; i++ {
  12. nums = append(nums, rand.Int())
  13. }
  14. return nums
  15. }
  16. // 动态扩容的slice
  17. func generateDynamic(n int) []int {
  18. rand.Seed(time.Now().UnixNano())
  19. nums := make([]int, 0)
  20. for i := 0; i < n; i++ {
  21. nums = append(nums, rand.Int())
  22. }
  23. return nums
  24. }
  25. func BenchmarkGenerateWithCap(b *testing.B) {
  26. for n := 0; n < b.N; n++ {
  27. generateWithCap(100000)
  28. }
  29. }
  30. func BenchmarkGenerateDynamic(b *testing.B) {
  31. for n := 0; n < b.N; n++ {
  32. generateDynamic(100000)
  33. }
  34. }
  1. D:\nyy_work\go_path\src\pkg007>go test -bench=. -benchmem -run=none
  2. goos: windows
  3. goarch: amd64
  4. pkg: pkg007
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkGenerateWithCap-12 394 3158336 ns/op 802818 B/op 1 allocs/op
  7. BenchmarkGenerateDynamic-12 345 3321054 ns/op 4654348 B/op 30 allocs/op
  8. PASS
  9. ok pkg007 3.888s
  • 结论是用cap初始化好的性能可以高一个数据量级

测试函数复杂度 不带cap的slice 动态扩容

  • 代码
  1. package main
  2. import (
  3. "math/rand"
  4. "testing"
  5. "time"
  6. )
  7. // 制定大的cap的切片
  8. func generateWithCap(n int) []int {
  9. rand.Seed(time.Now().UnixNano())
  10. nums := make([]int, 0, n)
  11. for i := 0; i < n; i++ {
  12. nums = append(nums, rand.Int())
  13. }
  14. return nums
  15. }
  16. // 动态扩容的slice
  17. func generateDynamic(n int) []int {
  18. rand.Seed(time.Now().UnixNano())
  19. nums := make([]int, 0)
  20. for i := 0; i < n; i++ {
  21. nums = append(nums, rand.Int())
  22. }
  23. return nums
  24. }
  25. func benchmarkGenerate(i int, b *testing.B) {
  26. for n := 0; n < b.N; n++ {
  27. generateDynamic(i)
  28. }
  29. }
  30. func BenchmarkGenerateDynamic1000(b *testing.B) { benchmarkGenerate(1000, b) }
  31. func BenchmarkGenerateDynamic10000(b *testing.B) { benchmarkGenerate(10000, b) }
  32. func BenchmarkGenerateDynamic100000(b *testing.B) { benchmarkGenerate(100000, b) }
  33. func BenchmarkGenerateDynamic1000000(b *testing.B) { benchmarkGenerate(1000000, b) }
  34. func BenchmarkGenerateDynamic10000000(b *testing.B) { benchmarkGenerate(10000000, b) }
  • 结果
  1. D:\nyy_work\go_path\src\pkg007>go test -bench=. -benchmem -run=none
  2. goos: windows
  3. goarch: amd64
  4. pkg: pkg007
  5. cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
  6. BenchmarkGenerateDynamic1000-12 29139 39159 ns/op 16376 B/op 11 allocs/op
  7. BenchmarkGenerateDynamic10000-12 4010 316369 ns/op 386297 B/op 20 allocs/op
  8. BenchmarkGenerateDynamic100000-12 350 3304217 ns/op 4654345 B/op 30 allocs/op
  9. BenchmarkGenerateDynamic1000000-12 37 31542581 ns/op 45188505 B/op 41 allocs/op
  10. BenchmarkGenerateDynamic10000000-12 4 263955575 ns/op 423503362 B/op 54 allocs/op
  11. PASS
  12. ok pkg007 10.298s
  • 结论就是 输入变为原来的10倍,单次耗时也差不多是上一级的10倍。说明这个函数的复杂度是线性的

string拼接的 bench

const letterBytes = “abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ”