单侧模拟mysql客户端工具_男神鹏:golang 单侧测试框架

1.单元测试框架调研

名称评分特点

testing

golang 官方自带

不支持断言和 mock

gocheck

近几年无更新

基于testing,支持断言,setup,suit。

testify

start :10000+

持续更新

基于testing,与gocheck 相似.suite包可以给每个测试用例进行前置操作和后置操作的功能(例如初始化和清空数据库)。

goconvey

start :5000+

持续更新

直接集成go test;

可以管理和运行测试用例;提供了丰富的断言函数;

支持很多 Web 界面特性。

gomonkey

start :2000+

持续更新

可以为全局变量、函数、过程、方法mock。

httpexpect

start :1400+

持续更新

适用于对http的clent进行测试,对服务端的回包进行打桩;支持对不同方法(get,post,head等)的构造,支持自定义返回值json。

sqlmock

start :2600+

持续更新

适用于和数据库的交互场景。可以创建模拟连接,编写原生sql 语句,编写返回值或者错误信息并判断执行结果和预设的返回值

2. 方案基本选型:testify + gomonkey; 附加 sqlmock

需要写单元测试的代码原则:

外部依赖少,代码又简单的代码。自然其成本和价值都是比较低的,可选;

外部依赖很少,业务复杂代码,最有价值写单元测试的。

testify基于gotesting编写,所以语法上、执行命令行与go test完全兼容。testify的 assert包提供了丰富的断言方法,避免testing的多层if else。此外提供了suite包,可以给每个测试用例进行前置操作和后置操作的功能,这个方便的功能,在前置操作和后置操作中去初始化和清空数据库。同时,还可以声明在这个测试用例周期内都有效的全局变量。

//安装testify

go getgithub.com/stretchr/testify

//更新testify

go get-u github.com/stretchr/testify

前提:

测试文件,以_test.go结尾,与被测文件放于相同目录

测试函数,函数名以Test开头,并且随后的第一个字符必须为大写字母或下划线,如:TestCategoryService_AddCategory

测试函数,参数为t testing.T;对于bench测试,参数为btesting.B

1.快速添加测试方法。右键方法,选择go to-test,生成test文件

2.给定对应case,使用assert 包中的方法添加断言,替换testing 的if else 判断。

assert 包还提供了更多断言方法

assert 断言库

require包提供了与assert包相同的全局函数,但它们不返回布尔结果,而是终止当前测试。

测试套件:

一种针对拥有多个实现的通用接口的测试,一个接口多个实现的时候不用重复的为特定版本书写测试。

前提:

测试套件文件名必须以 test.go 结尾。例:abc_test.go

文件中的函数以 Test,Benchmark,Example 开头。例子:TestAbc(),BenchmarkAbc(), ExampleAbc()。

func (s *SuiteType)SetUpSuite(c *C)-在测试套件启动前执行一次

func (s *SuiteType)SetUpTest(c *C)-在每个用例执行前执行一次

func (s *SuiteType)TearDownTest(c *C)-在每个用例执行后执行一次

func (s *SuiteType)TearDownSuite(c *C)--在测试套件用例都执行完成

基本格式:以asm 项目 collaborative 为例:

1. 定义测试套件:

//定义测试套件

type CollaborativeCategoryTestSuitestruct{

suite.Suite

//测试集需要用到的变量

baseCaller *collaborative.CallerInfo

//添加相关变量

addCategoryReq *collaborative.AddCategoryReq

addCategoryRsp *collaborative.AddCategoryRsp

agent *CollaborativeAgent

}

2. 定义测试入口:

//入口,正常的测试功能,将套件传递给suite.Run

func TestCollaborativeCategoryTestSuite(t *testing.T){

suite.Run(t,new(CollaborativeCategoryTestSuite))

}

测试套启动前初始化工作:SetUpSuite测试套件启动前执行一次,可做组件初始化和变量初始化,mock依赖调用的方法

//测试套件启动前执行一次,用到的变量和各种依赖组件的初始化

func (suite *CollaborativeCategoryTestSuite)SetupSuite(){

//agent 初始化

suite.agent,_ =NewCollaborativeAgent(c,m)

//参数初始化

suite.baseCaller =&collaborative.CallerInfo{

CorpID:313380573584411862,

UserID:312792371890801860,

Role:collaborative.CallerRole_Role_SP,

}

suite.addCategoryReq =&collaborative.AddCategoryReq{

BaseCaller:suite.baseCaller,

Category:&collaborative.Category{

Name:"lyricli1",

},

}

}

4.SetupTest也可以在每个用例执行前执行一次,这样就能在每个测试函数隐式调用。根据测试场景添加

func (suite *CollaborativeCategoryTestSuite)SetupTest(){

//将数据还原为初始状态,比如删除的数据之后的Expiry标志位还原,便于下次测试

err :=suite.agent.dbagent.Db.Model(&_type.Category{}).Where("id = ? ",suite.getCategoryReq.ID).Update(map[string]interface{}{"expiry":false}).Error

assert.NoError(suite.T(),err)

}

下面是测试函数的例子

//测试 添加分类

func (suite *CollaborativeCategoryTestSuite)TestAddCategory(){

req :=&collaborative.AddCategoryReq{}

res :=&collaborative.AddCategoryRsp{}

req =suite.addCategoryReq

suite.agent.AddCategory(context.TODO(),req,res)

assert.Equal(suite.T(),int32(common.CodeSucc),res.ErrCode)

//也可以用suite.True()判断

suite.True(int32(common.CodeSucc)==res.ErrCode,"add fail")

}

TearDownSuite 的在测试套件用例都执行完成的时候执行。比如清空本次测试的数据。

//所有测试中使用的拆卸变量,测试完清空数据

func (suite *CollaborativeCategoryTestSuite)TearDownSuite(){

err :=suite.agent.dbagent.Db.Debug().Exec("truncate TABLE categories;").Error

assert.NoError(suite.T(),err)

err =suite.agent.dbagent.Db.Debug().Exec("truncate TABLE category_sort_trees;").Error

assert.NoError(suite.T(),err)

}

注意:整个测试套件的执行顺序是 按照测试方法的名字的ASCII顺序来执行的,如果测试套件的执行想按照顺序去执行,那需要按照名字排序。

gomonkey 是 golang 的一款打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。使用思路,被测函数中需要使用的其他依赖函数,进行打桩处理。

gomonkey 支持的特性:(前四个比较常用)

支持为一个函数打一个桩

支持为一个函数打一个特定的桩序列

支持为一个成员方法打一个桩

支持为一个成员方法打一个特定的桩序列

支持为一个接口打一个桩

支持为一个接口打一个特定的桩序列

支持为一个函数变量打一个桩

支持为一个函数变量打一个特定的桩序列

支持为一个全局变量打一个桩

使用:

//安装 gomonkey

go getgithub.com/agiledragon/gomonkey

1. 函数打桩

gomonkey.ApplyFunc(target,double)

Patch是Monkey提供给用户用于函数打桩的API:

第一个参数是目标函数的函数名,target是被mock的目标函数。

第二个参数是桩函数的函数名,习惯用法是匿名函数或闭包,double是用户重写的函数

返回值是一个Patches对象指针,主要用于在测试结束时删除当前的补丁

// 一个简单的函数

func GetRecommendKey(moduleint)string{

returnfmt.Sprintf("pserver_recommend_%d",module)

}

//函数打桩

patches :=gomonkey.ApplyFunc(GetRecommendKey,func(int)string{

return"aaa"

})

defer p.Reset()

2. 函数打序列桩

ApplyFuncSeq第一个参数是函数名,第二个参数是特定的桩序列参数。

//序列桩

key1 :="hello test"

key2 :="hello golang"

key3 :="hello gomonkey"

outputs :=[]gomonkey.OutputCell{

{Values:gomonkey.Params{key1}},// 模拟函数的第1次输出

{Values:gomonkey.Params{key2}},// 模拟函数的第2次输出

{Values:gomonkey.Params{key3}},// 模拟函数的第3次输出

}

patches :=gomonkey.ApplyFuncSeq(GetRecommendKey,outputs)

output :=GetRecommendKey(1)

//第一次输出是否为指定的第一次打桩

assert.Equal(suite.T(),output,key1)

output =GetRecommendKey(2)

//第一次输出是否为指定的第二次打桩

assert.Equal(suite.T(),output,key2)

3.成员方法打桩

gomonkey.ApplyMethod(reflect.TypeOf(s), "target",double {mock方法实现})

s为目标变量,target为目标变量方法名,double为mock方法;同理double方法入参和出参需要和target方法保持一致。

这里注意,要被打桩的方式不能是私有方法,gomonkey通过反射是找不到的

在使用前,先要定义一个目标类的指针变量x

第一个参数是reflect.TypeOf(s)

第二个参数是字符串形式的函数名

返回值是一个Patches对象指针,主要用于在测试结束时删除当前的补丁

// 方法

func (u *CollaborativeAgent)NotifyServerStateChange(req *collaborative.ModifyServerStateReq)error {

logrus.Errorf("notifyServerStateChange req: %v",req)

ifreq ==nil{

returnnil

}

state :=req.State

ifstate ==collaborative.CollaborativeState_CS_Expired{

returnmq.PublishServerStateChange(req)

}else{

returnnil

}

}

//方法打桩

vars *CollaborativeAgent

p :=gomonkey.ApplyMethod(reflect.TypeOf(s),"NotifyServerStateChange",

func(u *CollaborativeAgent,ctx context.Context,req *collaborative.ModifyServerStateReq)error {

returnnil

})

4.成员方法打一个特定的序列桩

ApplyMethodSeq 第一个参数是目标类的指针变量的反射类型,第二个参数是字符串形式的方法名,第三参数是特定的桩序列参数。

key1 :=errors.New("existed")

key2 :=errors.New("not existed")

key3 :=error(nil)

outputs :=[]gomonkey.OutputCell{

{Values:gomonkey.Params{key1}},// 模拟函数的第1次输出

{Values:gomonkey.Params{key2}},// 模拟函数的第2次输出

{Values:gomonkey.Params{key3}},// 模拟函数的第3次输出

}

vars *CollaborativeAgent

patches :=gomonkey.ApplyMethodSeq(reflect.TypeOf(s),"NotifyServerStateChange",outputs)

output :=suite.agent.NotifyServerStateChange(req)

//第一次输出是否为指定的第一次打桩

assert.Equal(suite.T(),output,key1)

output =suite.agent.NotifyServerStateChange(req)

//第一次输出是否为指定的第二次打桩

assert.Equal(suite.T(),output,key2)

以ApplyMethod 为例,一个简单的demo说明帮助理解gomonkey打桩:

1.要被mock的方式如果是私有方法,gomonkey通过反射是找不到的

在go1.6版本中可以成功打桩的首字母小写的方法,当go版本升级后Monkey框架会显式触发panic,首字母小写的方法或函数不是public的。如果在UT测试中对首字母小写的方法或函数打桩的话,会导致重构的成本比较大。

2.macOS 10.15 syscall.Mprotect panic: permission denied

gomonkey issue

解决方案

3.Gomonkey对inline函数打桩无效

解决:通过命令行参数-gcflags=-l禁止inline( go test -gcflags=-l -v *_test.go -test.run 测试方法 )

sqlmock

适用于和数据库的交互场景。可以创建模拟连接,编写原生sql 语句,编写返回值或者错误信息并判断执行结果和预设的返回值,提供了完整的事务的执行测试框架,支持prepare参数化提交和执行的Mock方案。

//安装

go getgithub.com/DATA-DOG/go-sqlmock

1.通过 Sqlmock 可以获取 sql.DB 和 mock 对象

db,mock,err :=sqlmock.New()

2.以 MySQL 为例进行 mock:

gdb,err :=gorm.Open("mysql",db)

//关联

dbAgent :=&DBAgent{Db:gdb}

3.完整代码如下:

packagedata

import(

"git.code.oa.com/cloud_industry/asm/collaborative/type"

"github.com/DATA-DOG/go-sqlmock"

"github.com/jinzhu/gorm"

_ "github.com/jinzhu/gorm/dialects/mysql"

"github.com/stretchr/testify/assert"

"testing"

)

func TestDBAgent_GetCategory(t *testing.T){

db,mock,err :=sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))

assert.Nil(t,err)

defer db.Close()

gdb,err :=gorm.Open("mysql",db)

//关联

dbAgent :=&DBAgent{Db:gdb}

category :=&_type.Category{

Name:"aaa",

Description:"description",

ParentID:1,

Expiry:false,

Depth:1,

Disable:false,

}

rows :=sqlmock.

NewRows([]string{"id","name","description","parent_id","expiry","depth","disable"}).

AddRow(category.ID,category.Name,category.Description,category.ParentID,category.Expiry,category.Depth,category.Disable)

sql :="SELECT * FROM `categories` WHERE `categories`.`deleted_at` IS NULL AND ((`categories`.`id` = 1) AND (expiry = false)) ORDER BY `categories`.`id` ASC LIMIT 1"

mock.ExpectQuery(sql).WillReturnRows(rows)

c,err :=dbAgent.GetCategory(1)

assert.Nil(t,err)

assert.Equal(t,"aaa",c.Name)

}

指定查询的 SQL 语句,可以提供正则表达式,默认通过正则匹配。 WithArgs 指定 SQL 的参数, WillReturnRows 设置期待返回的查询结果。每次执行完 mock 用例,都需要执行 ExpectationsWereMet 来判断所有的 Sql mock 是否被满足。

FAQ:

could not match actual sql with expected regexp?

解决:

使用 regexp.QuoteMeta 方法转义SQL字符串中的所有正则表达式元字符。因此我们可以将 ExcectQuery 更改为 mock.ExpectQuery(regexp.QuoteMeta(sqlSelectAll)) 。

更改默认的SQL匹配器。创建模拟实例时,我们可以提供匹配器选项:sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))

测试覆盖率

1. 执行代码覆盖率测试如下:

cd test.go文件所在目录

go test -cover

cd -

2.使用 -coverprofile 标志来指定输出的文件( -coverprofile 标志自动设置 -cover 来启用覆盖率分析)

go test -coverprofile=test_coverage.out

//可以要求 覆盖率 按函数分解

go tool cover -func=size_coverage.out

3.获取 覆盖率信息注释的源代码 的HTML展示。 该显示由 -html 标志调用:

go tool cover -html=test_coverage.out

你可能感兴趣的:(单侧模拟mysql客户端工具)