Go 单元测试

简介: Go 单元测试

测试分为4个层次

  • 单元测试:对代码进行测试
  • 集成测试:对一个服务的接口测试
  • 端到端测试(链路测试):从一个链路的入口输入测试用例,验证输出的系统的结果
  • UI测试

image.png

常犯的错误:

  • 没有断言。没有断言的单测是没有灵魂的。单测的特征:
  • A:(Automatic,自动化):单元测试应该是全自动执行的,并且非交互式的
  • I:(Independent,独立性):为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
  • R:(Repeatable,可重复):单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。


单测


代码 bug 总是在所难免, 越早发现问题解决成本越低, 单测可以尽早的暴露错误。提高代码之路,使得项目更高质量的交付。起码有三个优点:

  • 提高代码质量 编写单测是自测的一部分,编写新代码时增加相应的单测,可以帮助我们发现大部分的bug,有助于减少联调时的调整,提高联调效率。
  • 花更少的时间进行功能测试 功能测试成本相对较高,因为经常需要执行一系列操作以验证结果是否符合预期。如果问题如果发现了问题,沟通和复测往往要花费很多的时间。
  • 花更少的时间进行回归测试 回归测试是为了避免在对应用程序进行更改时引入bug。测试人员不仅要测试他们的新特性,还要测试以前存在的特性,以验证之前实现的特性是否仍然像预期的那样运行。通过单元测试,可以在每次构建之后,重新运行整个测试流程,以确保新代码不会破坏已有功能
  • 测试异常场景 一些异常的场景QA不好构造,比如并发出款是否资金安全,事务异常相关测试等等。而问题经常出现在这些异常的场景,可能引发线上问题甚至是事故。而单元测试可通过mock的方式方便的模拟各种异常场景。


Go 单元测试工具

gomonkey

引入 gomonkey 有如下好处:

  • 隔离被测代码
  • 加速执行测试
  • 使执行变得确定
  • 模拟特殊情况

功能列表

  • 支持为一个函数打一个桩
  • 支持为一个函数打一个特定的桩序列
  • 支持为一个成员方法打一个桩
  • 支持为一个成员方法打一个特定的桩序列
  • 支持为一个函数变量打一个桩
  • 支持为一个函数变量打一个特定的桩序列
  • 支持为一个接口打桩
  • 支持为一个接口打一个特定的桩序列
  • 支持为一个全局变量打一个桩

函数打桩, 对变量的 mock 实现原理跟 gostub 一样都是通过 reflect 包实现的。除了 mock 变量,gomonkey 还可以直接 mock 导出函数/方法、mock 代码所在包的非导出函数

Go monkey Permission Denied 解决方案:https://github.com/eisenxp/macos-golink-wrapper


mv $GOROOT/pkg/tool/darwin_amd64/link $GOROOT/pkg/tool/darwin_amd64/original_link
cp https://github.com/eisenxp/macos-golink-wrapper/link $GOROOT/pkg/tool/darwin_amd64/link


下载文件,然后再 cp


wget https://raw.githubusercontent.com/eisenxp/macos-golink-wrapper/main/link


gomonkey 提供了如下 mock 方法:

  • ApplyGlobalVar(target, double interface{}):使用 reflect 包,将 target 的值修改为 double
  • ApplyFuncVar(target, double interface{}):检查 target 是否为指针类型,与 double 函数声明是否相同,最后调用 ApplyGlobalVar
  • ApplyFunc(target, double interface{}):修改 target 的机器指令,跳转到 double 执行
  • ApplyMethod(target reflect.Type, methodName string, double interface{}):修改 method 的机器指令,跳转到 double 执行
  • ApplyFuncSeq(target interface{}, outputs []OutputCell):修改 target 的机器指令,跳转到 gomonkey 生成的一个函数执行,每次调用会顺序从 outputs 取出一个值返回
  • ApplyMethodSeq(target reflect.Type, methodName string, outputs []OutputCell):修改 target 的机器指令,跳转到 gomonkey 生成的一个方法执行,每次调用会顺序从 outputs 取出一个值返回
  • ApplyFuncVarSeq(target interface{}, outputs []OutputCell):gomonkey 生成一个函数顺序返回 outputs 中的值,调用 ApplyGlobalVar


gomonkey 打桩失败的可能原因


  • gomonkey 不是并发安全的。如果有多协程并发对同一个目标的打桩的情况,则需要将之前的协程先优雅退出。
  • 打桩目标为内联的函数或成员方法。可通过命令行参数 -gcflags=-l (go1.10 版本之前)或-gcflags=all=-l(go1.10 版本及之后)关闭内联优化。
  • gomonkey 对于私有成员方法的打桩失败。go1.6 版本的反射机制支持私有成员方法的查询,而 go1.7 及之后的版本却不支持,所以当用户使用 go1.7 及之后的版本时,gomonkey 对于私有成员方法的打桩会触发异常。


goconvey


为全局变量打一个桩


package unittest
import (
 "testing"
 "github.com/agiledragon/gomonkey"
 "github.com/smartystreets/goconvey/convey"
)
var num = 10 //全局变量
func TestApplyGlobalVar(t *testing.T) {
 convey.Convey("TestApplyGlobalVar", t, func() {
  convey.Convey("change", func() {
   patches := gomonkey.ApplyGlobalVar(&num, 150)
   defer patches.Reset()
   convey.So(num, convey.ShouldEqual, 150)
  })
  convey.Convey("recover", func() {
   convey.So(num, convey.ShouldEqual, 10)
  })
 })
}


执行结果:


=== RUN   TestApplyGlobalVar
..
2 total assertions
--- PASS: TestApplyGlobalVar (0.00s)
PASS


为一个函数打桩


func networkCompute(a, b int) (int, error) {
 // do something in remote computer
 c := a + b
 return c, nil
}
func Compute(a, b int) (int, error) {
 sum, err := networkCompute(a, b)
 return sum, err
}
func TestFunc(t *testing.T) {
 // mock 了 networkCompute(),返回了计算结果2
 patches := gomonkey.ApplyFunc(networkCompute, func(a, b int) (int, error) {
  return 2, nil
 })
 defer patches.Reset()
 sum, err := Compute(1, 2)
 println("expected %v, got %v", 2, sum)
 if sum != 2 || err != nil {
  t.Errorf("expected %v, got %v", 2, sum)
 }
}


结果:


=== RUN   TestFunc
expected %v, got %v 2 3
    mock_func_test.go:91: expected 2, got 3
--- FAIL: TestFunc (0.00s)
FAIL

可以看到上面的结果,执行时失败的,mock 没有成功。

有时会遇到mock失效的情况,这个问题一般是内联导致的。

什么是内联?


为了减少函数调用时的堆栈等开销,对于简短的函数,会在编译时,直接内嵌调用的代码。

我们禁用下内联,然后执行,go test -v -gcflags=-l mock_func_test.go

执行结果:


=== RUN   TestFunc
expected %v, got %v 2 2
--- PASS: TestFunc (0.00s)
PASS


对于 go 1.10以下版本,可使用-gcflags=-l禁用内联,对于go 1.10及以上版本,可以使用-gcflags=all=-l。但目前使用下来,都可以。关于gcflags的用法,可以使用 go tool compile --help 查看 gcflags 各参数含义

相关文章
|
7天前
|
SQL 安全 数据库连接
《Go 简易速速上手小册》第6章:错误处理和测试(2024 最新版)(上)
《Go 简易速速上手小册》第6章:错误处理和测试(2024 最新版)
34 1
|
5月前
|
算法 测试技术 Go
【Go 编程实践】从零到一:创建、测试并发布自己的 Go 库
解释了为何需要开发自己的 Go 库,以及如何创建、测试和发布。文章以 Asiatz 库为例,详细阐述了创建目录、初始化项目、编写代码、测试、编写文档和发布等步骤,并强调了开发自己的 Go 库的优点,包括代码复用性、可维护性和可测试性。
236 0
【Go 编程实践】从零到一:创建、测试并发布自己的 Go 库
|
6月前
|
测试技术 Go
零代码上手测试:Go语言内置测试框架介绍
零代码上手测试:Go语言内置测试框架介绍
47 0
|
7月前
|
Java 测试技术 Go
Go测试之.golden 文件
Go测试之.golden 文件
56 0
|
6月前
|
关系型数据库 测试技术 Go
Go语言微服务框架 - 5.GORM库的适配sqlmock的单元测试
与此同时,我们也缺乏一个有效的手段来验证自己编写的相关代码。如果依靠连接到真实的MySQL去验证功能,那成本实在太高。那么,这里我们就引入一个经典的sqlmock框架,并配合对数据库相关代码的修改,来实现相关代码的可测试性。
71 0
|
2月前
|
IDE 测试技术 程序员
浅谈Go单元测试
浅谈Go单元测试
20 0
|
3月前
|
Cloud Native 测试技术 Go
云原生系列Go语言篇-编写测试Part 2
在花时间坠入优化的深渊之前,请明确程序需要进行优化。如果程序已经足够快,满足了响应要求,并且使用的内存量在接受范围之内,那么将时间花在新增功能和修复bug上会更好。业务的需求决定了何为"足够快"和"接受范围之内"
26 2
|
3月前
|
存储 Cloud Native 测试技术
云原生系列Go语言篇-编写测试Part 1
2000年以来,自动化测试的广泛应用可能比任何其他软件工程技术都更能提高代码质量。Go是一种专注于提高软件质量的语言和生态系统,很自然的在其标准库中包含了测试支持。
41 3
|
4月前
|
算法 Java 测试技术
go语言中的测试
go语言中的测试
46 0
|
4月前
|
缓存 自然语言处理 安全
Go Fuzzing(模糊测试)
Go Fuzzing(模糊测试)