一文搞懂Go语言单元测试

简介: 一文搞懂Go语言单元测试

文章目录



前言


平时根据需求写代码.人工进行测试往往不会面面俱到,还会因为需求的改变繁琐的进行测试


通过完成一个测试函数,可以大大简化测试的步骤,并且在需求该变的时候只需要改变一下测试的输入与期望


一、单元测试分类及其概念


1.基本分类


  • 测试函数 函数前缀为Test 主要用于测试程序的一些逻辑行为是否正确
  • 基准函数 函数名前缀为Benchmark 主要测试函数的性能
  • 示例函数 函数的前缀名为 Example 为文档提示示例文档


2.细说单元测试分类


①测试函数


  • 函数的基本测试
  • 函数的组测试
  • 函数的子测试
  • 测试函数覆盖率(也就是被测试函数有多少代码用于了执行)
  • 在进行测试的时候,要保证被测函数覆盖率为100%,测试函数覆盖率为60%以上,否则写的代码大部分用不到,需要优化
  • ①测试覆盖率可以使用go test -cover
  • ②go test -cover -coverprofile=c.out(将测试的结果存入文件c.out)
  • 然后使用go tool cover -html=c.out可以打开文件,显示哪些代码没有执行


②基准测试


  • 基准函数测试是做什么的:基准函数会自定义一个时间段用于执行代码,如果代码简洁,被测函数的执行次数需要成倍增加(直到达到基准测试函数给的预期,然后统计一共执行了多少轮,,每轮平均用时多少)
  • 执行基准函数时,首先要写基准函数
  • 基准测试函数的参数是:*testing.B对应的指针变量
  • 在测试函数内部要进行循环,循环的终止条件是b.N


③示例函数


二、结合代码细说每一种测试


1.基准测试


(1)基准测试函数需要注意的点


  进行基准测试时往往是对函数的算法进行测验,有时后一个算法在测试数据的基量不同时测试出的效果会不同
  我们需要对不同数量级的样本进行测试的时候。可以自己写一个盒子作为跳板,测试什么数量级的数据就只
  进行当前数量级的测试
  测试的时候命令为:go test -bench=.(将所有的跳板函数均执行一遍)
          或者=特定函数进行特定函数测试
          -benchtime=time 
          (在被测试函数在基准函数默认的时间内执行不完时可以拿这个参数进行时间的扩充)
  在进行基准函数测试的时候,可能需要进行一些前置工作,如果感觉前置工作浪费时间可以使用
  b.ResetTimer()对计时器进行重置


(2)基准测试代码


测试函数代码如下:


//文件名 subStr_test.go
package main
import (
  "reflect"
  "testing"
)
func BenchmarkSubStr(b *testing.B) {
  for i := 0; i < b.N; i++ {
    res := subStr("qwe:qw:es:wqe", ":")
    if !reflect.DeepEqual(res, []string{"qwe", "qw", "es", "wqe"}) {
      b.Errorf("不匹配")
    }
  }
}
func benchmarkFib(b *testing.B, n int) {
  for i := 0; i < b.N; i++ {
    Fib(n)
  }
}
func BenchmarkFib5(b *testing.B)  { benchmarkFib(b, 5) }
func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) }
func BenchmarkFib15(b *testing.B) { benchmarkFib(b, 15) }
func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) }


被测函数代码如下:


//文件名 subStr.go
package main
import (
  "fmt"
  "strings"
)
func subStr(str, stre string) []string {
  index := strings.Index(str, stre)
  var theSub []string
  for index >= 0 {
    // 先将分隔符前面的数据进行保存
    temp := str[:index]
    // 将字符串往后移
    str = str[index+1:]
    // 重新获取下标
    index = strings.Index(str, stre)
    if temp != "" {
      theSub = append(theSub, temp)
    } else {
      continue
    }
  }
  theSub = append(theSub, str)
  return theSub[:]
}
// 菲薄那契数列
func Fib(n int) int {
  if n < 2 {
    return n
  }
  return Fib(n-1) + Fib(n-2)
}
func main() {
  fmt.Println(subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
  fmt.Printf("%#v\n", subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
  fmt.Println(123)
}


  将以上两个文件放在同一目录下
  执行测试命令 go test
  得到测试结果
  goos: windows
  goarch: amd64
  cpu: Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz
  BenchmarkSubStr-8   1213681    1012 ns/op   352 B/op  14 allocs/op
  PASS
  ok    _/d_/Go语言学习笔记/go语言语法部分/go语言进阶语法/8.单元测试/基准测试 2.410s


2.组测试与子测试


  对于一个函数的测试,不仅仅是一组测试用例,所以我们需要写测试组,
  对一个函数进行测试,测试的时候一般用集合键值对构成,每一个键对应一组测试数据
  进行组测试的时候可能会有个别的组出错,所以使用子测试可以对某个案例进行单独测试
  这就是测试组与子测试的产生背景


测试函数如下:


package main
import (
  "reflect"
  "testing"
)
type testS struct {
  str  string
  ste  string
  want []string
}
//进行组测试,批量进行测试,如果有哪个地方不满足则进行异常抛出
// func TestSubStr(t *testing.T) {
//  testMap := map[string]testS{
//    "case_1": {"123:eqwe:123", ":", []string{"123", "eqwe", "123"}},
//    "case_2": {"13:eqwe3:1@23", "3:", []string{"1", "eqwe", "1@23"}},
//    "case_3": {"12@3@:@eq@we@:1@23", "@:", []string{"12@3", "@eq@we", "1@23"}},
//    "case_4": {"123:eq@we:1@2@3", "q@", []string{"123:e", "we:1@2@3"}},
//    "case_5": {"123:eqwe:123", "2", []string{"1", "3:eqwe:1", "3"}},
//    "case_6": {"123:eqwe:123", "eqwe", []string{"123:", ":123"}},
//  }
//  for k, v := range testMap {
//    res := subStr(v.str, v.ste)
//    if !reflect.DeepEqual(res, v.want) {
//      t.Errorf("%v want:%#v got:%#v", k, v.want, res)
//    }
//  }
// }
// 进行子测试,可以针对某个子样本进行测试
func TestSubStr(t *testing.T) {
  testMap := map[string]testS{
    "case_1": {"13:eqwe:123", ":", []string{"123", "eqwe", "123"}},
    "case_2": {"3:eqwe3:1@23", "3:", []string{"1", "eqwe", "1@23"}},
    "case_3": {"2@3@:@eq@we@:1@23", "@:", []string{"12@3", "@eq@we", "1@23"}},
    "case_4": {"123:eq@we:1@2@3", "q@", []string{"123:e", "we:1@2@3"}},
    "case_5": {"23:eqwe:123", "2", []string{"1", "3:eqwe:1", "3"}},
    "case_6": {"123:eqwe:123", "eqwe", []string{"123:", ":123"}},
  }
  for k, v := range testMap {
    t.Run(k, func(t *testing.T) {
      res := subStr(v.str, v.ste)
      if !reflect.DeepEqual(res, v.want) {
        t.Errorf("want:%#v got:%#v", v.want, res)
      }
    })
  }
}


待测函数如下:


package main
import (
  "fmt"
  "strings"
)
func subStr(str, stre string) []string {
  index := strings.Index(str, stre)
  var theSub []string
  for index >= 0 {
    // 先将分隔符前面的数据进行保存
    temp := str[:index]
    // 将字符串往后移
    str = str[index+len(stre):]
    // 重新获取下标
    index = strings.Index(str, stre)
    if temp != "" {
      theSub = append(theSub, temp)
    } else {
      continue
    }
  }
  theSub = append(theSub, str)
  return theSub[:]
}
func main() {
  fmt.Println(subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":c"))
  fmt.Printf("%#v\n", subStr("q:w:ec:wer:cd:scn:cj:c:is:icc:cin:si", ":"))
  fmt.Println(123)
}


(1).组测试结果分析及命令:


组测试命令仍使用 go test


--- FAIL: TestSubStr (0.00s)
  --- FAIL: TestSubStr/case_1 (0.00s)
     subStr_test.go:46: want:[]string{"123", "eqwe", "123"} got:[]string{"13", "eqwe", "123"}        
  --- FAIL: TestSubStr/case_2 (0.00s)
       subStr_test.go:46: want:[]string{"1", "eqwe", "1@23"} got:[]string{"eqwe", "1@23"}
  --- FAIL: TestSubStr/case_3 (0.00s)
       subStr_test.go:46: want:[]string{"12@3", "@eq@we", "1@23"} got:[]string{"2@3", "@eq@we", "1@23"}
  --- FAIL: TestSubStr/case_5 (0.00s)
       subStr_test.go:46: want:[]string{"1", "3:eqwe:1", "3"} got:[]string{"3:eqwe:1", "3"}
  FAIL
  exit status 1
  FAIL    _/D_/Go语言学习笔记/go语言语法部分/go语言进阶语法/8.单元测试/组测试与子测试     0.155s


(2).子测试结果分析及命令:


  例如单独测试case_1,使用的命令为
  go test -v -run=TestSubStr/case_1(等号后面跟的是上面组测试失败案例FAIL后面的目录)


=== RUN   TestSubStr
=== RUN   TestSubStr/case_1
    subStr_test.go:46: want:[]string{"123", "eqwe", "123"} got:[]string{"13", "eqwe", "123"}
--- FAIL: TestSubStr (0.00s)
    --- FAIL: TestSubStr/case_1 (0.00s)
FAIL
exit status 1
FAIL    _/D_/Go语言学习笔记/go语言语法部分/go语言进阶语法/8.单元测试/组测试与子测试     0.186s


三、pprof调试工具


1.对主函数进行传参


(1)os.Args


package main1
import (
  "fmt"
  "os"
)
func main() {
  // os.Args可以在执行函数的时候传递参数,但是对于-name=xxx没有办法将其解析
  if os.Args != nil {
    for index, temp := range os.Args {
      fmt.Println("第", index+1, "个参数是:", temp)
    }
  }
  fmt.Println("hello")
}


(2)flag.Args


相对于os.Args来说,flag.Args使用起来更加方便


package main
import (
  "flag"
  "fmt"
  "time"
)
func main() {
  // 第一个参数为对应的参数,第二个参数为默认值,第三个参数为提示
  // 返回的是对应类型的指针
  // name := flag.String("name", "Tom", "输入name")
  // sex := flag.Bool("sex", true, "是不是男性")
  // age := flag.Int("age", 10, "年龄")
  // flag.Parse()
  // fmt.Println(*name, *sex, *age)
  var name string
  var age int
  var sex bool
  flag.StringVar(&name, "name", "Tom", "输入name")
  flag.BoolVar(&sex, "sex", true, "是不是男性")
  flag.IntVar(&age, "age", 10, "年龄")
  tim := flag.Duration("time", time.Hour, "时间")
  // 将输入的数据进行解析,不使用这句话的话不能获取到name sex age等属性对应的值
  flag.Parse()
  fmt.Println(name, sex, age, *tim)
  fmt.Println(flag.Args())  //以切片的方式返回命令行之外的其他参数
  fmt.Println(flag.NArg())  //返回命令行之外的其他参数的个数
  fmt.Println(flag.NFlag()) //返回使用命令行的参数的个数
}


2.pprof性能调优


pprof调试工具进行调试,主要看待测模块在内存于时间上的效益
调试的时候只会显示耗费时间空间较多的代码段.
生成调试代码块的文件:go run xx.exe -cpu....
使用go语言工具查看代码块所存在的问题:go tool pprof cpu.pprof


package main
import (
  "flag"
  "fmt"
  "os"
  "runtime/pprof"
  "time"
)
// 一段有问题的代码
func logicCode() {
  var c chan int
  for {
    select {
    case v := <-c:
      fmt.Printf("recv from chan, value:%v\n", v)
    default:
    }
  }
}
func main() {
  var isCPUPprof bool
  var isMemPprof bool
  //获取接收到的参数
  flag.BoolVar(&isCPUPprof, "cpu", false, "turn cpu pprof on")
  flag.BoolVar(&isMemPprof, "mem", false, "turn mem pprof on")
  flag.Parse()
  //判断进行什么测试,然后执行分支,并将信息保存到相应的文件内。
  if isCPUPprof {
    file, err := os.Create("./cpu.pprof")
    if err != nil {
      fmt.Printf("create cpu pprof failed, err:%v\n", err)
      return
    }
    pprof.StartCPUProfile(file)
    defer pprof.StopCPUProfile()
  }
  for i := 0; i < 8; i++ {
    go logicCode()
  }
  time.Sleep(20 * time.Second)
  if isMemPprof {
    file, err := os.Create("./mem.pprof")
    if err != nil {
      fmt.Printf("create mem pprof failed, err:%v\n", err)
      return
    }
    pprof.WriteHeapProfile(file)
    file.Close()
  }
}


总结


这里对常用的测试函数,子测试进行了详细的讲解,对于pprof参数调优仅仅做了个介绍,可以根据自己的兴趣在网上找一部分pprof的例子深入学习,进行自动化测试非常方便。

目录
相关文章
|
8天前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
1天前
|
安全 Java Go
探索Go语言在高并发环境中的优势
在当今的技术环境中,高并发处理能力成为评估编程语言性能的关键因素之一。Go语言(Golang),作为Google开发的一种编程语言,以其独特的并发处理模型和高效的性能赢得了广泛关注。本文将深入探讨Go语言在高并发环境中的优势,尤其是其goroutine和channel机制如何简化并发编程,提升系统的响应速度和稳定性。通过具体的案例分析和性能对比,本文揭示了Go语言在实际应用中的高效性,并为开发者在选择合适技术栈时提供参考。
|
5天前
|
运维 Kubernetes Go
"解锁K8s二开新姿势!client-go:你不可不知的Go语言神器,让Kubernetes集群管理如虎添翼,秒变运维大神!"
【8月更文挑战第14天】随着云原生技术的发展,Kubernetes (K8s) 成为容器编排的首选。client-go作为K8s的官方Go语言客户端库,通过封装RESTful API,使开发者能便捷地管理集群资源,如Pods和服务。本文介绍client-go基本概念、使用方法及自定义操作。涵盖ClientSet、DynamicClient等客户端实现,以及lister、informer等组件,通过示例展示如何列出集群中的所有Pods。client-go的强大功能助力高效开发和运维。
24 1
|
6天前
|
SQL 关系型数据库 MySQL
Go语言中使用 sqlx 来操作 MySQL
Go语言因其高效的性能和简洁的语法而受到开发者们的欢迎。在开发过程中,数据库操作不可或缺。虽然Go的标准库提供了`database/sql`包支持数据库操作,但使用起来稍显复杂。为此,`sqlx`应运而生,作为`database/sql`的扩展库,它简化了许多常见的数据库任务。本文介绍如何使用`sqlx`包操作MySQL数据库,包括安装所需的包、连接数据库、创建表、插入/查询/更新/删除数据等操作,并展示了如何利用命名参数来进一步简化代码。通过`sqlx`,开发者可以更加高效且简洁地完成数据库交互任务。
13 1
|
11天前
|
XML JSON Go
微服务架构下的配置管理:Go 语言与 yaml 的完美结合
微服务架构下的配置管理:Go 语言与 yaml 的完美结合
|
5天前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
7天前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
8天前
|
安全 Go API
go语言中的Atomic操作与sema锁
在并发编程中,确保数据一致性和程序正确性是关键挑战。Go语言通过协程和通道提供强大支持,但在需精细控制资源访问时,Atomic操作和sema锁变得至关重要。Atomic操作确保多协程环境下对共享资源的访问是不可分割的,如`sync/atomic`包中的`AddInt32`等函数,底层利用硬件锁机制实现。sema锁(信号量锁)控制并发协程数量,其核心是一个uint32值,当大于零时通过CAS操作实现锁的获取与释放;当为零时,sema锁管理协程休眠队列。这两种机制共同保障了Go语言并发环境下的数据完整性和程序稳定性。
|
9天前
|
算法 Go
Go 语言 实现冒泡排序
冒泡排序是大家熟知的经典算法。在Go语言中实现它,关键在于理解其核心思想:通过不断比较并交换相邻元素,让序列中的最大值像泡泡一样“浮”至顶端。每一轮比较都能确定一个最大值的位置。外层循环控制排序轮数,内层循环负责比较与交换。随着每轮排序完成,未排序部分逐渐缩小,直至整个数组有序。以下是Go语言实现示例及说明。
17 1
|
11天前
|
设计模式 算法 测试技术
动态支付策略:Go 语言中策略模式的妙用
动态支付策略:Go 语言中策略模式的妙用