男神鵬: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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM