<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文說盡Golang單元測(cè)試實(shí)戰(zhàn)的那些事兒

          共 10870字,需瀏覽 22分鐘

           ·

          2022-05-19 20:24

          導(dǎo)語(yǔ)?|?單元測(cè)試,通常是單獨(dú)測(cè)試一個(gè)方法、類或函數(shù),讓開發(fā)者確信自己的代碼在按預(yù)期運(yùn)行,為確保代碼可以測(cè)試且測(cè)試易于維護(hù)。騰訊后臺(tái)開發(fā)工程師張力結(jié)合了公司級(jí)漏洞掃描系統(tǒng)洞犀在DevOps上探索的經(jīng)驗(yàn),以Golang為例,列舉了編寫單元測(cè)試需要的工具和方法,然后針對(duì)寫單測(cè)遇到的各種依賴問題,詳細(xì)介紹了通過Mock的方式解決各種常用依賴,方便讀者在寫go語(yǔ)言UT的時(shí)候,遇到依賴問題,能夠快速找到解決方案。最后再和大家探討一下關(guān)于單元測(cè)試上的一些思考。


          一、前言


          單元測(cè)試,通常是單獨(dú)測(cè)試一個(gè)方法、類或函數(shù),讓開發(fā)者確信自己的代碼在按預(yù)期運(yùn)行,為確保代碼可以測(cè)試且測(cè)試易于維護(hù)。另一方面,DevOps里提倡自動(dòng)化測(cè)試,并且主張?jiān)皆绨l(fā)現(xiàn)代價(jià)越小。關(guān)于單元測(cè)試的更多思考,可以看看本文最后一節(jié)。



          本文結(jié)合了公司級(jí)漏洞掃描系統(tǒng)洞犀在DevOps上探索的經(jīng)驗(yàn),以Golang為例,列舉了編寫單元測(cè)試需要的工具和方法,然后針對(duì)寫單測(cè)遇到的各種依賴問題,提出相應(yīng)的解決辦法,并展示了自動(dòng)化單元測(cè)試的結(jié)果。最后再和大家探討一下關(guān)于單元測(cè)試上的一些思考。


          二、測(cè)試工具與方法


          1.測(cè)試框架


          相信大家都熟悉go內(nèi)置了go test測(cè)試框架來執(zhí)行和管理測(cè)試用例。通過文件名_test.go結(jié)尾來表示測(cè)試文件,通過函數(shù)以Test開頭并只有一個(gè)參數(shù)*testing.T來表示一個(gè)測(cè)試函數(shù)。例如:


          // sample_test.go
          package sample_test
          import ( "testing" )
          func?TestDownload(t?*testing.T)?{}func?TestUpload(t?*testing.T)?{}

          而其中測(cè)試框架testing的類型*T提供了一系列方法,例如主要會(huì)用到的下面三個(gè)方法:


          • t.Fatal:會(huì)讓測(cè)試函數(shù)立刻返回錯(cuò)誤

          • t.Error:會(huì)輸出錯(cuò)誤并記錄失敗,但任然會(huì)繼續(xù)運(yùn)行

          • t.Log:輸出 debug 信息,go test -v參數(shù)下有效


          除此之外,還有其他用的比較多的測(cè)試包。例如斷言包"github.com/stretchr/testify/assert",比如如果想判斷返回的錯(cuò)誤是否是空,如果用原生方法會(huì)是:


          if err != nil t.Errorf("got error %v", err)}

          但用assert包只需要一行代碼就可以實(shí)現(xiàn)上述功能,而且可以輸出具體錯(cuò)誤代碼行:assert.Nil(t, err)。另外還有封裝了testing的測(cè)試框架https://github.com/smartystreets/goconvey,里面包含了子測(cè)試斷言等功能。

          2.表格驅(qū)動(dòng)測(cè)試

          表格驅(qū)動(dòng)測(cè)試通過定義一組不同的輸入,可以讓代碼得到充分的測(cè)試,同時(shí)也能有效地測(cè)試負(fù)路徑。?例如下面函數(shù)會(huì)判斷參數(shù)類型,如果是int就乘以二,如果是string就先轉(zhuǎn)成int然后乘以二,如果是其他類型就返回錯(cuò)誤:


          func twice(i interface{}) (int, error) { switch v := i.(type) { case int:  return v * 2, nil case string:  value, err := strconv.Atoi(v)  if err != nil {   return 0, errors.Wrapf(err, "invalid string num %s", v)  }  return value * 2, nil default:  return 0, errors.New("unknown type") }}

          可以看到該函數(shù)有多個(gè)分支,如果要覆蓋到不同分支,就需要不同類型輸入,那么這就很適合表格驅(qū)動(dòng)測(cè)試:

          func Test_twice(t *testing.T) { type args struct {  i interface{} } tests := []struct {  name    string  args    args  want    int  wantErr bool }{  {   name: "int",   args: args{i: 10},   want: 20,  },  {   name: "string success",   args: args{i: "11"},   want: 22,  },{   name:    "string failed",   args:    args{i: "aaa"},   wantErr: true,  },  {   name:    "unknown type",   args:    args{i: []byte("1")},   wantErr: true,  }, } for _, tt := range tests {  t.Run(tt.name, func(t *testing.T) {   got, err := twice(tt.args.i)   if (err != nil) != tt.wantErr {    t.Errorf("twice() error = %v, wantErr %v", err, tt.wantErr)    return   }if got != tt.want {    t.Errorf("twice() got = %v, want %v", got, tt.want)   }  }) }}

          上面還用到了go test的子測(cè)試功能t.Run(name string, subTest func(t *T))。如果想在一個(gè)測(cè)試函數(shù)里面執(zhí)行多個(gè)測(cè)試用例,例如要同時(shí)測(cè)試一個(gè)函數(shù)的返回成功和失敗等各種情況,那么可以使用子測(cè)試來區(qū)分不同情況。

          另外,上面表格測(cè)試代碼框架是用Goland自動(dòng)生成的,自己只需要填寫tests數(shù)組就行了。點(diǎn)擊函數(shù)名然后右鍵,選擇generate,然后選擇test for function就會(huì)自動(dòng)生成測(cè)試函數(shù)了。不過上面生成的函數(shù)沒有校驗(yàn)返回的錯(cuò)誤內(nèi)容,如有需要可以自己稍微修改一下。

          三、解決常見的依賴等問題


          解決常見的依賴等問題目前有兩種思路:


          • 通過mock方式替換實(shí)際依賴,并通過打樁操作其返回內(nèi)容。

          • 通過本地啟動(dòng)一個(gè)模擬依賴環(huán)境,比如模擬redis服務(wù)等,然后直接訪問模擬服務(wù)。


          下面幾小節(jié)詳細(xì)介紹了上述兩種辦法在不通場(chǎng)景下的應(yīng)用,其中替換函數(shù)或方法、依賴接口類型和mysql數(shù)據(jù)庫(kù)依賴對(duì)應(yīng)了第一種思路;訪問訪問http接口、mysql數(shù)據(jù)庫(kù)依賴和redis依賴對(duì)應(yīng)了上面第二條思路。


          四、訪問?http?接口


          代碼里經(jīng)常會(huì)遇到要訪問http接口的情況,這時(shí)如果在測(cè)試代碼里不做處理直接訪問,可能遇到環(huán)境不同訪問不通等問題。為此go標(biāo)準(zhǔn)庫(kù)內(nèi)置了專門用于測(cè)試http服務(wù)的包net/http/httptest,不過我們這里并不用它來測(cè)試http服務(wù),而是用來模擬要請(qǐng)求的http服務(wù)。


          基本流程是先創(chuàng)建一個(gè)路由器,然后注冊(cè)一個(gè)響應(yīng)函數(shù)用來模擬要請(qǐng)求的服務(wù):


          mux := http.NewServeMux()mux.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { writer.Write([]byte(`{"code":0,"msg":"ok"}`))})

          接著啟動(dòng)這個(gè)服務(wù),httptest會(huì)真的在localhost啟動(dòng)它,然后這個(gè)URL就是要訪問的服務(wù)地址了。

          server := httptest.NewServer(mux)defer server.Close()url?:=?server.URL


          五、替換函數(shù)或方法


          大家用的最多的應(yīng)該就是monkey補(bǔ)丁庫(kù)了,可以用它來替換各種函數(shù)和方法,使用起來非常方便,這類庫(kù)原理大致相同,通過運(yùn)行時(shí)用unsafe包替換函數(shù)地址來實(shí)現(xiàn)。比如https://github.com/agiledragon/gomon

          key,不過這次我們用公司內(nèi)部同源測(cè)試團(tuán)隊(duì)封裝的monkey庫(kù)來演示ngmock。


          首先是替換函數(shù),新建一個(gè)函數(shù)mock對(duì)象,參數(shù)有*testing.T和要mock的函數(shù)。比如被測(cè)函數(shù)需要調(diào)用db.New新建一個(gè)DB,那么下面就mock了db.New函數(shù)。


          dbNewMock := ngmock.MockFunc(t, db.New)defer?dbNewMock.Finish()

          然后在執(zhí)行被測(cè)函數(shù)之前,設(shè)置mock函數(shù)接收什么參數(shù),并且要返回什么,比如下面指定接收一個(gè)任意參數(shù)并且讓db.New返回指定錯(cuò)誤。該設(shè)置默認(rèn)只會(huì)生效一次,如果要生效多次或者一直生效可以配置次數(shù)。

          dbNewMock.Mock(ngmock.Any()).Return(nil,?errors.New("fake?err")).AnyTimes()


          接下來就是執(zhí)行被測(cè)函數(shù)函數(shù)來驗(yàn)證是否生效了,這里用到了上面提到的另一個(gè)測(cè)試框架convey,convey.Convey同*T.Run(),convey.So是 assert。


          func TestNewDBs(t *testing.T) { convey.Convey("TestNewDBs", t, func() {  dbNewMock := ngmock.MockFunc(t, db.New)  defer dbNewMock.Finish()  convey.Convey("TestNewDBs failed", func() {   dbNewMock.Mock(ngmock.Any()).Return(nil, errors.New("fake err")).AnyTimes()   dbs, err := NewDBs(DbUrl{}) // 執(zhí)行被測(cè)函數(shù)   convey.So(dbs, convey.ShouldResemble, &DBs{})   convey.So(err, convey.ShouldNotBeNil)   convey.So(err.Error(), convey.ShouldEqual, "fake err") // 驗(yàn)證是否生效  }) })}

          可以看到,mock依賴函數(shù)之后執(zhí)行被測(cè)函數(shù),會(huì)返回我們?cè)O(shè)置的錯(cuò)誤fake error,在調(diào)用完成獲得返回錯(cuò)誤之后可以判斷一下是否是我們?cè)O(shè)置的錯(cuò)誤。還可以mock結(jié)構(gòu)體方法,使用方式和上面類似,第二個(gè)參數(shù)傳結(jié)構(gòu)體或者指針,第三個(gè)是mock模式:

          execCmdMock := ngmock.MockStruct(t, exec.Cmd{}, ngmock.Silent)defer?execCmdMock.Finish()


          mock模式主要有兩種:


          • Silent結(jié)構(gòu)體內(nèi)沒有mock的方法返回類型默認(rèn)值,調(diào)用沒有mock的方法不會(huì)報(bào)錯(cuò),最后對(duì)調(diào)用方法的統(tǒng)計(jì)不會(huì)報(bào)錯(cuò)。

          • KeepOrigin結(jié)構(gòu)體內(nèi)沒有mock的方法按照原方法邏輯返回?cái)?shù)據(jù),調(diào)用沒有mock的方法不會(huì)報(bào)錯(cuò),最后對(duì)調(diào)用方法的統(tǒng)計(jì)不會(huì)報(bào)錯(cuò)。


          在mock方法時(shí),需要指定方法名,比如下面就mock了該結(jié)構(gòu)體的Output方法,方法如果有參數(shù)的話,可以在后面加上參數(shù)。其他的就和前面一樣了。


          execCmdMock.Mock("Output").Return([]byte("1"),?nil)


          如果在MacOS上執(zhí)行測(cè)試遇到了permission denied的錯(cuò)誤,這是 MacOS保護(hù)機(jī)制導(dǎo)致的,具體解決辦法見https://github.com/eisenx

          p/macos-golink-wrapper 。


          六、依賴接口類型


          如果依賴的數(shù)據(jù)是接口類型,那么可以很方便的通過依賴注入的方式傳入測(cè)試用的接口實(shí)現(xiàn)來替換原始依賴。go 官方出品的gomock 可以根據(jù)接口定義自動(dòng)生成相應(yīng)實(shí)現(xiàn)的mock樁代碼:https://github.com/golang/

          mock。gomock庫(kù)會(huì)有個(gè)二進(jìn)制文件mockgen用來生成代碼,?比如文件里有一些接口定義:


          // interfaces.go
          // Encoder 編碼器type Encoder interface { Encode(obj interface{}, w io.Writer) error}//go:generate?mockgen?-destination=./mockdata/interfaces_mock.go?-package=mockdata?-self_package=./mockdata?-source=interfaces.go

          ?


          可以執(zhí)行mockgen來生成上述接口,具體命令如上,-destination指定生成文件名,-package是生成文件包名,-self_package指定生成的包路徑,-source就是源接口文件路徑名。如果最后不指定接口名的話,會(huì)生成所有接口或者可以指定要生成的接口,多個(gè)用逗號(hào)連接。?當(dāng)然也可以讀取標(biāo)準(zhǔn)庫(kù)的接口:mockgen database/sql/driver Conn,Driver樁代碼生成好了之后,就可以調(diào)用代碼里類似 NewMockXXXX(ctrl)方法來創(chuàng)建mock對(duì)象,如下所示,這樣創(chuàng)建的encoderMock實(shí)現(xiàn)了上面的Encoder接口,接下來就用這個(gè)encoderMock來初始化被測(cè)函數(shù)依賴的接口即可。

          ctrl := gomock.NewController(t) // *testing.Tdefer ctrl.Finish()// mockdata 是上面生成的樁代碼目錄encoderMock?:=?mockdata.NewMockEncoder(ctrl)

          在調(diào)用被測(cè)函數(shù)之前,需要先打樁:我們希望如果encoderMock在執(zhí)行Encode方法時(shí)傳入會(huì)兩個(gè)指定參數(shù),那么就執(zhí)行指定的函數(shù)并返回:

          codecMock.EXPECT().Encode(gomock.Any(), gomock.Any()).DoAndReturn(func(obj interface{}, w io.Writer) error { w.Write([]byte("test_data")) return nil})

          接下來執(zhí)行被測(cè)函數(shù),當(dāng)實(shí)際調(diào)用到Encode方法時(shí),就會(huì)執(zhí)行我們?cè)O(shè)置的函數(shù)。看起來和上面一節(jié)的替換函數(shù)和方法類似是吧?這種希望當(dāng)調(diào)用函數(shù)Encode()并且參數(shù)一致,那么就執(zhí)行指定邏輯的方式,就是打樁(stub)。打樁過程還可以配置執(zhí)行次數(shù)和執(zhí)行順序等,如果不知道打樁函數(shù)具體會(huì)被傳入什么參數(shù)可以用gomock.Any()來代替。通過打樁可以控制依賴接口的行為,解決測(cè)試時(shí)接口依賴的問題。

          七、mysql?數(shù)據(jù)庫(kù)依賴


          數(shù)據(jù)庫(kù)依賴也是經(jīng)常要遇到的一個(gè)問題,如何解決測(cè)試過程中的依賴呢?我這里總結(jié)了兩種辦法:?首先是sqlmock:https://github.com/DATA-DOG/go-sqlmock。看到mock字眼大家大概也知道它是怎么使用的了,也是通過對(duì)執(zhí)行sql語(yǔ)句打樁來完成測(cè)試。首先初始化mock對(duì)象,返回第一個(gè)是*sql.DB,用來傳給被測(cè)代碼依賴的db,第二個(gè)就是mock對(duì)象,用來設(shè)置打樁代碼。控制sqlDB的行為。


          sqlDB,?dbMock,?err?:=?sqlmock.New()


          具體使用項(xiàng)目文檔里有,我這里簡(jiǎn)單說一下:比如下面一個(gè)函數(shù)執(zhí)行一些sql語(yǔ)句,先調(diào)用Begin創(chuàng)建事務(wù),然后分別Query和Exec執(zhí)行sql,最后如果返回錯(cuò)誤則Rollback否則Commit。


          func testFunc(db *sql.DB) error { tx, err := db.Begin() if err != nil {  return err } defer func() {  if err != nil {   tx.Rollback()  } else {   tx.Commit()  } }() rows, err := tx.Query("select * from test where id > 10") if err != nil {  return err } defer rows.Close() for rows.Next() {  // 省略 }  if _, err := tx.Exec("update test set num = num +1"); err != nil {  return err } return nil}


          那么針對(duì)上面函數(shù),編寫測(cè)試用例如下。其中打樁代碼按照上面順序,希望先執(zhí)行Begin;然后執(zhí)行Query,并且希望sql語(yǔ)句滿足正則select .* from test并返回兩行結(jié)果;然后執(zhí)行Exec,希望 sql 滿足正則update test并返回錯(cuò)誤;最后執(zhí)行Rollback。接下來執(zhí)行被測(cè)函數(shù),如果被測(cè)函數(shù)按照打樁代碼的順序執(zhí)行相應(yīng)sql的話就會(huì)返回指定內(nèi)容,否則就會(huì)報(bào)錯(cuò)。


          func Test_testFunc(t *testing.T) { convey.Convey("Test_testFunc exec failed", t, func() {  sqlDB, dbMock, err := sqlmock.New()  convey.So(err, convey.ShouldBeNil)
          dbMock.ExpectBegin() // sql支持正則匹配 dbMock.ExpectQuery("select .* from test"). WillReturnRows(sqlmock.NewRows([]string{"id", "num"}). AddRow(1, 10).AddRow(2, 20)) dbMock.ExpectExec("update test").WillReturnError(errors.New("fake error")) dbMock.ExpectRollback()
          err = testFunc(sqlDB) // 執(zhí)行被測(cè)函數(shù)
          convey.So(err, convey.ShouldNotBeNil) convey.So(err.Error(), convey.ShouldEqual, "fake error") // 驗(yàn)證打樁是否生效
          // 確認(rèn)所有打樁都被調(diào)用 convey.So(dbMock.ExpectationsWereMet(), convey.ShouldBeNil) })}


          有時(shí)候我們的代碼不會(huì)直接使用*sql.DB,而是用到一些第三方 ORM 框架,那么需要想辦法讓這些框架使用我們的 mock db,比如對(duì)于 gorm 框架,可以這么配置:


          sqlDB, dbMock, err := sqlmock.New()// "gorm.io/driver/mysql"// "gorm.io/gorm"db, err = gorm.Open(mysql.New(mysql.Config{Conn: sqlDB}), &gorm.Config{})


          談到gorm框架,那么問題來了,如果我不直接操作*sql.DB而是用的框架,但我不知道最后生成的sql是什么那該怎么辦?或者說被測(cè)函數(shù)有一堆sql語(yǔ)句,一個(gè)一個(gè)打樁起來實(shí)在是太麻煩。那么對(duì)于這種情況如果能有一個(gè)本地?cái)?shù)據(jù)庫(kù)環(huán)境就好了,省去了打樁的麻煩,但是如果是mysql這種DB的話,本地建一個(gè)最快也是用容器跑才行。那么有沒有更輕量化的辦法呢?


          可以本地臨時(shí)創(chuàng)建一個(gè)sqlite數(shù)據(jù)庫(kù)來代替當(dāng)前依賴的數(shù)據(jù)庫(kù)比如mysql等,sqlite是可以在本地直接跑的輕量級(jí)數(shù)據(jù)庫(kù),常見sql語(yǔ)句增刪改查什么的和mysql區(qū)別不大。不過需要注意的是目前所有的go sqlite驅(qū)動(dòng)都是基于CGO的,因?yàn)閟qlite使用C寫的。所以引用這些驅(qū)動(dòng)會(huì)導(dǎo)致測(cè)試前程序編譯速度變慢和跨平臺(tái)支持問題,不過目前測(cè)試在MacOS和linux上是沒有問題的。


          如下所示首先創(chuàng)建一個(gè)臨時(shí)的sqlite gorm框架DB,其中連接地址置空,這樣在關(guān)閉db之后數(shù)據(jù)庫(kù)也會(huì)自動(dòng)刪除。之后就可以正常使用了。它底層使用的是這個(gè)驅(qū)動(dòng)github.com/mattn/go-sqlite3。


          import( "gorm.io/driver/sqlite" "gorm.io/gorm")db, err := gorm.Open(sqlite.Open(""), &gorm.Config{})


          如果使用場(chǎng)景只是增刪改查什么的,問題不會(huì)很大,我目前遇到的和 mysql 不兼容的就是create table a like b這種 sql。而且如果不直接執(zhí)行 sql 而用框架取調(diào)用相關(guān)函數(shù)的話,兼容性會(huì)好很多。


          八、redis?依賴


          很多項(xiàng)目還會(huì)依賴redis數(shù)據(jù)庫(kù),那么這種怎么解決依賴問題呢?可以使用miniredis庫(kù)解決問題:https://github.com/alicebob/miniredis 。


          miniredis是一個(gè)純GO寫的測(cè)試用的redis服務(wù),它支持絕大多數(shù)redis命令,具體可以看項(xiàng)目介紹。使用起來很簡(jiǎn)單,直接調(diào)用Run函數(shù)啟動(dòng)一個(gè)測(cè)試服務(wù),服務(wù)對(duì)象的Addr()方法返回服務(wù)連接地址。接下來可以就可以拿著這個(gè)地址替換當(dāng)前依賴了。


          import "github.com/alicebob/miniredis/v2"import "github.com/go-redis/redis/v8"
          mr, err := miniredis.Run()addr := mr.Addr() // redis服務(wù)的tcp連接地址
          // 比如創(chuàng)建一個(gè)客戶端opt, err := redis.ParseURL("redis://:@" + mr.Addr())cli := redis.NewClient(opt)


          九、執(zhí)行測(cè)試用例前后設(shè)置


          有時(shí)需要在測(cè)試之前或之后進(jìn)行額外的設(shè)置(setup)或拆卸(teardown),為此testing包提供了TestMain函數(shù)作為測(cè)試文件的入口。如下所示,該文件的測(cè)試用例都會(huì)在m.Run里運(yùn)行,如果成功返回0否則非零,因此可以判斷執(zhí)行是否成功。值得注意的是最后應(yīng)該使用code作為os.Exit參數(shù)退出。


          func TestMain(m *testing.M) { code := m.Run() if code == 0 {  TearDone(true) } else {  TearDone(false) } os.Exit(code)}
          func TearDone(isSuccess bool) { fmt.Println("Global test environment tear-down") if isSuccess { fmt.Println("[ PASSED ]") } else { fmt.Println("[ FAILED ]") }}


          十、忽略指定目錄


          有時(shí)需要忽略指定目錄,例如自動(dòng)生成的樁代碼,proto文件等,以提高覆蓋率,那么對(duì)于下面的測(cè)試命令:go test -v -covermode=count -coverprofile=coverage_unit.out '-gcflags=all=-N -l' ./... 如果要忽略掉mockdata目錄的話,后面加上grep -v mockdata即可:


          go test -v -covermode=count -coverprofile=coverage_unit.out  '-gcflags=all=-N -l' `go list ./... | grep -v /mockdata`


          然后可以運(yùn)行g(shù)o tool cover -html=coverage_unit.out -o cover.html,生成網(wǎng)頁(yè)版報(bào)告,查看覆蓋率情況。當(dāng)然還有一個(gè)比較tricky的方法,如果生成的樁代碼僅限于某個(gè)包內(nèi)使用,那么直接把樁代碼文件名改成_test.go后綴的就行了。


          十一、關(guān)于單元測(cè)試的思考


          1.單測(cè)的意義


          先必須承認(rèn)有了單元測(cè)試之后,增加了代碼質(zhì)量的保障。而且在做修改和重構(gòu)的時(shí)候,也能降低心智負(fù)擔(dān),相信大家都體驗(yàn)過對(duì)一堆沒有單測(cè)的代碼做修改時(shí)心里都會(huì)有點(diǎn)打怵,生怕改出什么問題。


          但是對(duì)于沒有單元測(cè)試的人來說,剛開始寫單測(cè)無疑是讓人非常頭大,簡(jiǎn)直寸步難行。因?yàn)橐呀?jīng)維護(hù)的代碼可能在設(shè)計(jì)上就很難測(cè)試,各種耦合各種依賴沒有抽象混在一起,一行代碼成百上千行,這些都加深了接入單元測(cè)試的難度和工作量。而由于沒有質(zhì)量保證又不敢動(dòng)這些祖?zhèn)鞔a,從而導(dǎo)致陷入死循環(huán)。


          但總得想辦法改變現(xiàn)狀,最近看了公司內(nèi)部技術(shù)論壇的測(cè)試專題,之后也是跟各路大神學(xué)習(xí)到了一些東西。首先可以先讓重要邏輯代碼有測(cè)試。其次就是關(guān)注代碼設(shè)計(jì)問題,對(duì)新增代碼堅(jiān)持寫單側(cè),我在碼客上看到有前輩說,**UT 不是用來找BUG的,而是通過UT來改良設(shè)計(jì),從而提升代碼質(zhì)量,降低BUG數(shù)量。**反之如果UT不好寫,說明代碼結(jié)構(gòu)混亂,出現(xiàn)BUG的概率也變高。


          2.不能為了單測(cè)而單測(cè)


          單元測(cè)試覆蓋率高真的可以確保質(zhì)量嗎?是否能消除BUG?這個(gè)按我個(gè)人經(jīng)驗(yàn)其實(shí)是不能完全保證的。首先得考慮單測(cè)覆蓋代碼分支是否完備?有時(shí)候?yàn)榱送祽兄粶y(cè)了主路徑,對(duì)于其他負(fù)路徑等沒有測(cè)試,那么肯定會(huì)有問題的。其次測(cè)試環(huán)境和線上實(shí)際環(huán)境的潛在差異可能也會(huì)導(dǎo)致代碼BUG沒測(cè)試出來。我遇到過在寫打樁代碼的時(shí)候,懶得校驗(yàn)參數(shù),直接用mock.Any代替,導(dǎo)致做集成測(cè)試的時(shí)候發(fā)現(xiàn)參數(shù)傳錯(cuò)了,寫這種單測(cè)除了浪費(fèi)時(shí)間之外基本上也發(fā)現(xiàn)不了什么問題。

          3.有沒有更好折中方案


          有時(shí)候函數(shù)邏輯比較復(fù)雜導(dǎo)致插樁過程繁瑣,或者有些依賴不方便 mock,那么是否能在執(zhí)行測(cè)試用例的時(shí)候創(chuàng)建一個(gè)本地測(cè)試環(huán)境,里面包含了各種依賴,這樣或許會(huì)方便很多。比如上一節(jié)介紹解決依賴的辦法里有提到為了解決DB依賴,可以臨時(shí)創(chuàng)建一個(gè)sqlite數(shù)據(jù)庫(kù),或者啟動(dòng)一個(gè)容器來模擬執(zhí)行環(huán)境。



          作者簡(jiǎn)介


          張力

          騰訊后臺(tái)開發(fā)工程師,負(fù)責(zé)高危服務(wù)掃描系統(tǒng)建設(shè)。



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù)?ebook?獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人人干人人叉人人操 | 国产免费淫秽视频 | 福利偷拍 | 精品免费一区二区三区在线赤裸 | 75精品福利导航 |