zoukankan      html  css  js  c++  java
  • 男神鹏: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

     

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

    1. //安装testify
    2. go get github.com/stretchr/testify
    3. //更新testify
    4. go get -u github.com/stretchr/testify

    前提:

    • 测试文件,以_test.go结尾,与被测文件放于相同目录
    • 测试函数,函数名以Test开头,并且随后的第一个字符必须为大写字母或下划线,如:TestCategoryService_AddCategory
    • 测试函数,参数为t testing.T;对于bench测试,参数为b testing.B

     

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

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

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

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

    测试套件:

     

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

    1. 测试套件文件名必须以 test.go 结尾。例:abc_test.go
    2. 文件中的函数以 Test,Benchmark,Example 开头。例子:TestAbc(),BenchmarkAbc(), ExampleAbc()。
    1. func (s *SuiteType) SetUpSuite(c *C) - 在测试套件启动前执行一次
    2. func (s *SuiteType) SetUpTest(c *C) - 在每个用例执行前执行一次
    3. func (s *SuiteType) TearDownTest(c *C) - 在每个用例执行后执行一次
    4. func (s *SuiteType) TearDownSuite(c *C) - - 在测试套件用例都执行完成

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

    1. 定义测试套件:
     
    1. //定义测试套件
    2. type CollaborativeCategoryTestSuite struct {
    3. suite.Suite
    4. //测试集需要用到的变量
    5. baseCaller *collaborative.CallerInfo
    6. //添加相关变量
    7. addCategoryReq *collaborative.AddCategoryReq
    8. addCategoryRsp *collaborative.AddCategoryRsp
    9. agent *CollaborativeAgent
    10. }
    2. 定义测试入口:
     
    1. //入口,正常的测试功能,将套件传递给suite.Run
    2. func TestCollaborativeCategoryTestSuite(t *testing.T) {
    3. suite.Run(t, new(CollaborativeCategoryTestSuite))
    4. }
    1. 测试套启动前初始化工作:SetUpSuite测试套件启动前执行一次,可做组件初始化和变量初始化,mock依赖调用的方法
    1. //测试套件启动前执行一次,用到的变量和各种依赖组件的初始化
    2. func (suite *CollaborativeCategoryTestSuite) SetupSuite() {
    3. //agent 初始化
    4. suite.agent, _ = NewCollaborativeAgent(c, m)
    5. //参数初始化
    6. suite.baseCaller = &collaborative.CallerInfo{
    7. CorpID: 313380573584411862,
    8. UserID: 312792371890801860,
    9. Role: collaborative.CallerRole_Role_SP,
    10. }
    11. suite.addCategoryReq = &collaborative.AddCategoryReq{
    12. BaseCaller: suite.baseCaller,
    13. Category: &collaborative.Category{
    14. Name: "lyricli1",
    15. },
    16. }
    17. }

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

    1. func (suite *CollaborativeCategoryTestSuite) SetupTest() {
    2. //将数据还原为初始状态,比如删除的数据之后的Expiry标志位还原,便于下次测试
    3. err := suite.agent.dbagent.Db.Model(&_type.Category{}).Where("id = ? ",suite.getCategoryReq.ID).Update(map[string]interface{}{ "expiry": false}).Error
    4. assert.NoError(suite.T(), err)
    5. }
    1. 下面是测试函数的例子
    1. //测试 添加分类
    2. func (suite *CollaborativeCategoryTestSuite) TestAddCategory() {
    3. req :=&collaborative.AddCategoryReq{}
    4. res :=&collaborative.AddCategoryRsp{}
    5. req = suite.addCategoryReq
    6. suite.agent.AddCategory(context.TODO(),req,res)
    7. assert.Equal(suite.T(), int32(common.CodeSucc), res.ErrCode)
    8. //也可以用suite.True()判断
    9. suite.True(int32(common.CodeSucc)==res.ErrCode,"add fail")
    10. }
    1. TearDownSuite 的在测试套件用例都执行完成的时候执行。比如清空本次测试的数据。
    1. //所有测试中使用的拆卸变量,测试完清空数据
    2. func (suite *CollaborativeCategoryTestSuite) TearDownSuite() {
    3. err := suite.agent.dbagent.Db.Debug().Exec("truncate TABLE categories;").Error
    4. assert.NoError(suite.T(), err)
    5. err = suite.agent.dbagent.Db.Debug().Exec("truncate TABLE category_sort_trees;").Error
    6. assert.NoError(suite.T(), err)
    7. }

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

    gomonkey

     

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

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

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

    使用:

     
    1. //安装 gomonkey
    2. go get github.com/agiledragon/gomonkey
    1. 函数打桩
     

    gomonkey.ApplyFunc(target,double)
    Patch是Monkey提供给用户用于函数打桩的API:

    • 第一个参数是目标函数的函数名,target是被mock的目标函数。
    • 第二个参数是桩函数的函数名,习惯用法是匿名函数或闭包,double是用户重写的函数
    • 返回值是一个Patches对象指针,主要用于在测试结束时删除当前的补丁
    1. // 一个简单的函数
    2. func GetRecommendKey(module int) string {
    3. return fmt.Sprintf("pserver_recommend_%d", module)
    4. }
    5. //函数打桩
    6. patches :=gomonkey.ApplyFunc(GetRecommendKey, func(int) string {
    7. return "aaa"
    8. })
    9. defer p.Reset()
    2. 函数打序列桩
     

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

    1. //序列桩
    2. key1 := "hello test"
    3. key2 := "hello golang"
    4. key3 := "hello gomonkey"
    5. outputs := []gomonkey.OutputCell{
    6. {Values: gomonkey.Params{key1}},// 模拟函数的第1次输出
    7. {Values: gomonkey.Params{key2}},// 模拟函数的第2次输出
    8. {Values: gomonkey.Params{key3}},// 模拟函数的第3次输出
    9. }
    10. patches :=gomonkey.ApplyFuncSeq(GetRecommendKey, outputs)
    11. output := GetRecommendKey(1)
    12. //第一次输出是否为指定的第一次打桩
    13. assert.Equal(suite.T(),output,key1)
    14. output = GetRecommendKey(2)
    15. //第一次输出是否为指定的第二次打桩
    16. 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对象指针,主要用于在测试结束时删除当前的补丁
    1. // 方法
    2. func (u *CollaborativeAgent) NotifyServerStateChange(req *collaborative.ModifyServerStateReq) error {
    3. logrus.Errorf("notifyServerStateChange req: %v", req)
    4. if req == nil {
    5. return nil
    6. }
    7. state := req.State
    8. if state == collaborative.CollaborativeState_CS_Expired {
    9. return mq.PublishServerStateChange(req)
    10. } else {
    11. return nil
    12. }
    13. }
    14. //方法打桩
    15. var s *CollaborativeAgent
    16. p := gomonkey.ApplyMethod(reflect.TypeOf(s), "NotifyServerStateChange",
    17. func(u *CollaborativeAgent, ctx context.Context, req *collaborative.ModifyServerStateReq) error {
    18. return nil
    19. })
    4.成员方法打一个特定的序列桩
     

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

    1. key1 := errors.New("existed")
    2. key2 := errors.New("not existed")
    3. key3 := error(nil)
    4. outputs := []gomonkey.OutputCell{
    5. {Values: gomonkey.Params{key1}},// 模拟函数的第1次输出
    6. {Values: gomonkey.Params{key2}},// 模拟函数的第2次输出
    7. {Values: gomonkey.Params{key3}},// 模拟函数的第3次输出
    8. }
    9. var s *CollaborativeAgent
    10. patches := gomonkey.ApplyMethodSeq(reflect.TypeOf(s), "NotifyServerStateChange", outputs)
    11. output := suite.agent.NotifyServerStateChange(req)
    12. //第一次输出是否为指定的第一次打桩
    13. assert.Equal(suite.T(),output,key1)
    14. output = suite.agent.NotifyServerStateChange(req)
    15. //第一次输出是否为指定的第二次打桩
    16. assert.Equal(suite.T(),output,key2)

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

    其他的方法使用
    FAQ:

    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方案。

    1. //安装
    2. go get github.com/DATA-DOG/go-sqlmock
    1.通过 Sqlmock 可以获取 sql.DB 和 mock 对象
     
    1. db, mock, err := sqlmock.New()
    2.以 MySQL 为例进行 mock:
     
    1. gdb, err := gorm.Open("mysql", db)
    2. //关联
    3. dbAgent := &DBAgent{Db: gdb}
    3.完整代码如下:
     
    1. package data
    2. import (
    3. "git.code.oa.com/cloud_industry/asm/collaborative/type"
    4. "github.com/DATA-DOG/go-sqlmock"
    5. "github.com/jinzhu/gorm"
    6. _ "github.com/jinzhu/gorm/dialects/mysql"
    7. "github.com/stretchr/testify/assert"
    8. "testing"
    9. )
    10. func TestDBAgent_GetCategory(t *testing.T) {
    11. db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
    12. assert.Nil(t,err)
    13. defer db.Close()
    14. gdb, err := gorm.Open("mysql", db)
    15. //关联
    16. dbAgent := &DBAgent{Db: gdb}
    17. category := &_type.Category{
    18. Name:"aaa",
    19. Description:"description",
    20. ParentID:1,
    21. Expiry:false,
    22. Depth:1,
    23. Disable:false,
    24. }
    25. rows := sqlmock.
    26. NewRows([]string{"id","name", "description", "parent_id", "expiry", "depth","disable"}).
    27. AddRow(category.ID,category.Name, category.Description, category.ParentID, category.Expiry, category.Depth, category.Disable)
    28. sql := "SELECT * FROM `categories` WHERE `categories`.`deleted_at` IS NULL AND ((`categories`.`id` = 1) AND (expiry = false)) ORDER BY `categories`.`id` ASC LIMIT 1"
    29. mock.ExpectQuery(sql).WillReturnRows(rows)
    30. c, err := dbAgent.GetCategory(1)
    31. assert.Nil(t,err)
    32. assert.Equal(t,"aaa",c.Name)
    33. }

     

    指定查询的 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. 执行代码覆盖率测试如下:

     
    1. cd test.go文件所在目录
    2. go test -cover
    3. cd -

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

     
    1. go test -coverprofile=test_coverage.out
    2. //可以要求 覆盖率 按函数分解
    3. go tool cover -func=size_coverage.out

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

     
    1. go tool cover -html=test_coverage.out

  • 相关阅读:
    npm版本管理
    spring框架学习笔记5:SpringAOP示例
    spring框架学习笔记4:SpringAOP实现原理
    spring框架学习笔记3:使用注解代替配置文件
    spring框架学习笔记2:配置详解
    spring框架学习笔记1:搭建测试
    struts2框架学习笔记7:struts2标签
    struts2框架学习笔记6:拦截器
    struts2框架学习笔记5:OGNL表达式
    struts2框架学习笔记4:获取参数
  • 原文地址:https://www.cnblogs.com/lyp0626/p/14148229.html
Copyright © 2011-2022 走看看