当前位置:网站首页>Go 单元测试入门实践
Go 单元测试入门实践
2022-06-29 10:36:00 【华为云】
Go 单元测试
Go中不同文件的单元测试代码,写在其对应的 xxx_test.go 文件,该单元测试文件可以包含三种类型的函数,单元测试函数、基准测试函数和示例函数。本文只介绍其中的单元测试函数。
本文将从不同的需求场景出发,用具体的例子速览Go单元测试的编写。
(ps:对于函数以及方法打桩推荐使用 gomonkey,bou.ke 的 monkey 框架证书已经失效)
1. 单元测试的基本构成
简单测试函数示例:
// 文件名 demo.gopackage testutilimport "errors"type calculator struct {}func (c *calculator) multiplication(a, b int) (int, error) { return a*b, nil}func (c *calculator) Division(a, b int) (int, error) { if b == 0 { return 0, errors.New("error: divide by 0") } return a/b, nil}// 在同一个目录中创建 demo_test.go 文件package testutilimport ( "github.com/stretchr/testify/require" "testing")// 基本构成:构造输入,执行函数,判断结果func Test_calculator_Division(t *testing.T) { var cal calculator // 定义输入 numA := 2 numB := 1 // 预期结果 want := 2 // 执行函数 res, err := cal.Division(numA, numB) // 校验 if err != nil { t.Errorf("Division() error = %v, wantErr: nil", err) } if res != want { t.Errorf("Division() result = %v, want %v", res, want) }}跳过某些测试:
测试函数增加如下判断,执行 go test -short 会跳过以下测试用例
func Test_calculator_Division(t *testing.T) { if testing.Short() { t.Skip("short模式下会跳过该测试用例") } ...}创建多个测试用例:
- 子测试:t.Run(“case name”, func(t *testing){…}),其中 case name 用于区分不同的测试用例
- 表格驱动测试:for range { t.Run(“case name”, func(t *testing){…}) }
- 并行处理测试:在 func 开头加入 t.Parallel();注:默认情况下同一个package里面是串行运行,不同package之间是并行执行
- 使用帮助函数提取重复逻辑:在 _test.go文件中定义的非测试函数开头加入 t.Helper() 可以使报错更清晰,报错时会输出调用者的信息,而不是只输出被调用函数的信息
- PS: t.Error 和 t.Fatal 区别在于,前者遇到错误不停
// 多个测试用例// 表格驱动// 并行执行func Test_calculator_Division(t *testing.T) { // 包内也并行执行 t.Parallel() // 定义输入 type args struct { a int b int } // 多个测试用例:用例名称、入参、预期结果、预期是否出错 tests := []struct { name string args args want int wantErr bool }{ {"case name", args{2, 1}, 2, false}, } // 遍历测试用例 for _, tt := range tests { // 使用t.Run()执行子测试 t.Run(tt.name, func(t *testing.T) { c := &calculator{} got, err := c.Division(tt.args.a, tt.args.b) if (err != nil) != tt.wantErr { t.Errorf("Division() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("Division() got = %v, want %v", got, tt.want) } }) }}多个测试都依赖某变量:
// 可以用来初始化全局变量;将多个测试函数都依赖的实体(变量,数据库,GRPC服务等),在测试开始前初始化好,从而避免重复代码// 每个代码包可以有一个 TestMain, 它会包中所有测试开始前执行func TestMain(m *testing.M) { setUp() // 自己实现的资源初始化代码,一般是对全局变量进行赋值 code := m.Run() tearDown() // 清理资源,如:关闭DB,删除临时文件,关闭 http 连接等 os.Exit(code)}快速生成测试文件:
使用 Goland 右键 Generate 生成函数或整个文件的测试代码
使用 gotests 生成:gotests -all -w demo.go
测试覆盖率:
通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。
执行:go test -cover 或者 go tool cover -html=c.out
可以使用 goland 的右键执行 test with coverage,即可可视化看到包中测试覆盖情况
testify: 断言 /require /assert:
testify是一个社区非常流行的Go单元测试工具包,其中使用最多的功能就是它提供的断言工具—testify/assert 或 testify/require。(也可以选用功能更加丰富 goconver)
// slice 的对比判断if !reflect.DeepEqual(got, s.want) { t.Errorf("expected:%#v, result:%#v", s.want, res)}// 使用 testify 只需一行判断assert.Equal(t, res, s.want)有多个断言语句时:
// 创建 assert 对象 assert := assert.New(t)// 无需再传入 Testing.T 参数了assert.Equal(res, want)require 拥有 assert 所有断言函数,它们的唯一区别就是 — require 遇到失败的用例会立即终止本次测试。
2. 接口打桩
Mock:接口测试
除了网络和数据库等外部依赖之外,我们在开发中也会经常用到各种各样的接口类型。使用 gomock 可以较好地为接口打桩,gomonkey也可以,只是接口测试编写稍微复杂一些。
Mockgen
mockgen 命令用来为给定一个包含要mock的接口的Go源文件,生成mock类源代码。
安装:$GOPATH/bin已经加入到环境变量中;GO111MODULE=on; go get github.com/golang/mock/[email protected]
运行: 源码模式和反射模式
mockgen -source=db.go -destination=mocks/db_mock.go -package=mocks测试函数写法:
func TestGetFromDB(t *testing.T) { // 创建gomock控制器,用来记录后续的操作信息 ctrl := gomock.NewController(t) // 调用mockgen生成代码中的NewMockDB方法 m := mocks.NewMockDB(ctrl) // 打桩(stub) // 当传入Get函数的参数为 expect input 时返回 1 和 nil m. EXPECT(). xxx(gomock.Eq("expect input")). // 函数方法 xxx,指定输入参数=“expect input” Return(1, nil). // 返回值 1,nil Times(1) // 调用次数 // 传入mock接口 m,给函数调用 if v := GetFromDB(m, "expect input"); v != 1 { t.Fatal() } // 如果函数调用时传入的参数不是 “expect input”,那么打桩不会生效,而是调用原函数}入参可以指定特定值,也可以使用以下的方法指定:
- gomock.Eq(value):表示一个等价于value值的参数
- gomock.Not(value):表示一个非value值的参数
- gomock.Any():表示任意值的参数
- gomock.Nil():表示空值的参数
返回值可以设置不同的返回方式:
- Return():返回指定值
- Do(func):执行操作,忽略返回值
- DoAndReturn(func):执行并返回指定值
设置期望被调用的次数:
- Times() 断言 Mock 方法被调用的次数。
- MaxTimes() 最大次数。
- MinTimes() 最小次数。
- AnyTimes() 任意次数(包括 0 次)。
指定调用顺序:
// 指定顺序gomock.InOrder( m.EXPECT().FunctionName("1"), m.EXPECT().FunctionName("2"), m.EXPECT().FunctionName("3"),)注意:
对接口进行 mock 之后,需要传入该mock之后的对象。如果测试函数无法传入mock之后的对象,则无法测试。
func GetFromDB(key string) int { db := NewDB() if value, err := db.Get(key); err == nil { return value } return -1}// 如果是这个方法,无法传入mock后的 db,因此该方法无法测试3.全局变量打桩
Stub:全局变量
GoStub 也是一个单元测试中的打桩工具,它支持为全局变量、函数等打桩。一般在单元测试中只会使用它来为全局变量打桩,Stub为函数打桩不太方便。另外全局变量也可以用 gomonkey 打桩。
使用示例:
func TestGetConfig(t *testing.T) { // 为全局变量configFile打桩,给它赋值为一个指定字符串 stubs := gostub.Stub(&configFile, "./test.txt") defer stubs.Reset() // 执行要测试的函数 data, err := GetConfig() if err != nil { t.Fatal() } // 此时函数GetConfig()中使用的全局变量 configFile 就是我们上面设置的值}4.函数打桩
Monkey:函数、方法
(由于 license 问题,推荐使用更强大的并且一直在更新的 gomonkey 包替代)
monkey 是一个Go单元测试中十分常用的打桩工具,其原理可以参考这篇 Monkey Patching in Go: 译文。
Gomonkey: 函数、方法、变量
gomonkey is a library to make monkey patching in unit tests easy, and the core idea of monkey patching comes from Bouke, you can read this blogpost for an explanation on how it works.
gomonkey 打桩的原理与 bou.ke 的 monkey 是一样的,并且 gomonkey 功能更加丰富:支持为函数、成员方法、全局变量、接口打桩等。下面以新版的 github.com/agiledragon/gomonkey/v2 为例,介绍不同的打桩方法
函数:ApplyFunc
// ApplyFunc(参数:要被打桩的函数;定义新的函数以及其输出)patches := ApplyFunc(myPackage.MyFunction, func(_ string, _ ...string) (string, error) { return "expected result", nil})defer patches.Reset()// 后续执行被打桩的函数,得到的函数结果即为 "expected result", nilres, err := myPackage.MyFunction("any string input", "any string input")成员方法:ApplyMethodFunc
instance := &myStruct{}var p *myStruct// ApplyMethodFunc(参数:成员的指针;成员方法名称;定义替换的函数)patches := ApplyMethodFunc(p, "TheMethodName", func(_ int) error { return nil})defer patches.Reset()// 后续调用任意该成员实列的该方法,都只执行上述定义的打桩函数,结果返回 nilerr := instance.TheMethodName(1222)全局变量:ApplyGlobalVar
// ApplyGlobalVar(参数:变量地址,变量打桩的值)patches := ApplyGlobalVar(&num, 150)defer patches.Reset()接口:需要组合复用上述的 ApplyFunc 和 ApplyMethod
(对接口的打桩,gomonkey 用起来还是没有上述的 mock 自然)
// 使用 ApplyFunc 将返回接口的函数指向一个实现对象patches := ApplyFunc(TheFunctionReturnInterface, func(_ string) myInterface { return interfaceInstance})defer patches.Reset()// 然后通过 ApplyMethod 改变上述实现对象的行为patches.ApplyMethod(interfaceInstance, "TheMethodName",func(_ *xxx, _ string) (string, error) { return "expected result", nil})其它类型的打桩可以参考 gomonkey 仓库中的 example
5.HTTP打桩
httptest:
服务端模拟
// 模拟 http serverfunc NewLocalHTTPSTestServer(handler http.Handler) (*httptest.Server, error) { ts := httptest.NewUnstartedServer(handler) cert, err := tls.LoadX509KeyPair("server.crt", "server.key") if err != nil { return nil, err } ts.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} ts.StartTLS() // 此处启动 https 服务,如果是 ts.Start() 则是 HTTP 服务 return ts, nil}// client 调用测试func TestLocalHTTPSserver(t *testing.T) { handlerFunc := http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Hello, client") // 对请求的处理逻辑 } ) ts, err := NewLocalHTTPSTestServer(handlerFunc) assert.Nil(t, err) defer ts.Close() // ts.URL 为上面模拟server的访问地址,可以通过替换 listener 改变 res, err := http.Get(ts.URL) assert.Nil(t, err) // 如果想要替换 url: // 新建server时先不启动,也就是去掉 ts.StartTLS() // mylistener, err := net.Listen("tcp", "127.0.0.1:8080") // ts.Listener.Close() // ts.Listener = mylistener // ts.StartTLS() greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() assert.Nil(t, err) assert.Equal(t, "Hello, client", string(greeting))}边栏推荐
- X-FRAME-OPTIONS web page hijacking vulnerability
- What is the experience of working in an IT company in Japan?
- 【HBZ分享】Mysql的InnoDB原理
- [digital signal modulation] realize signal modulation and demodulation based on am+fm+dsb+ssb, including Matlab source code
- ModbusTCP协议WIFI无线学习型单路红外模块(小壳版)
- 斐波那锲数列与冒泡排序法在C语言中的用法
- Design and implementation of IDS
- TTL串口学习型红外遥控模块可扩展成网络控制
- Easydss is deployed on Disk C, and the video playback cannot be played normally. How to solve this problem?
- (JS) imitate an instanceof method
猜你喜欢

多线程实现客户端与服务端通信(初级版本)

What are the main factors that affect the heat dissipation of LED packaging?

Online sql to htmltable tool

什么?漫画居然能免费看全本了,这还不学起来一起做省钱小能手

Google Earth engine (GEE) - Gedi L2a vector canopy top height (version 2) global ecosystem data set

math_ Mathematical expression & deformation of equation equation & accumulation of combined operation skills / means

Spark - one to one correspondence between task and partition and detailed explanation of parameters

(JS) filter out keys with value greater than 2 in the object

西门子S7-200SMART控制步进电机的具体方法及示例程序

(JS) array de duplication
随机推荐
云原生开发必备:首个通用无代码开发平台 iVX 编辑器
Creating postgre enterprise database by ArcGIS
ModbusTCP协议网络学习型单路红外模块(双层板)
Specific method and example program of Siemens s7-200smart control stepping motor
如何识别出轮廓准确的长和宽
Encore une fois, le chemin de l'amélioration de redis Cloud
适合小白的树莓派opencv4.0安装
9 款好用到爆的 JSON 处理工具,极大提高效率!
【HBZ分享】Semaphore 与 CountDownLatch原理
Interview questions of Tencent automation software test of CSDN salary increase secret script (including answers)
喜报|海泰方圆通过CMMI-3资质认证,研发能力获国际认可
Qt学习04 Hello Qt
Course design for the end of the semester: product sales management system based on SSM
(JS) handwritten deep copy
Adding sharding sphere5.0.0 sub tables to the ruoyi framework (adding custom sub table policies through SPI)
【无标题】我在密谋一件大事
ModbusTCP协议网络学习型单路红外模块(中壳版)
(JS) filter out keys with value greater than 2 in the object
Today in history: musk was born; Microsoft launches office 365; The inventor of Chua's circuit was born
Data analysis method and Thinking: funnel analysis