Docker 雲平臺下 Go 語言單元測試實踐

數人云團隊在最近5個月的golang項目實踐中所積累的單元測試的一些經驗,團隊項目的覆蓋率從最初的無到現在的接近90%,想着我們遇到的問題大家可能也會遇到,所以在這裏把實踐寫出來,期待大家的反饋和建議。

1. Go語言單元測試框架

首先我們來了解一下go語言單元測試的基礎知識。

go語言的單元測試採用內置的測試框架,通過引入testing包以及go test來提供測試功能。

在源代碼包目錄內,所有以_test.go爲後綴名的源文件被go test認定爲測試文件,這些文件不包含在go build的代碼構建中,而是單獨通過 go test來編譯,執行。

通常對於測試用例,go test有着以下規約:

  • 每個測試函數必須導入testing包。測試函數有如下的命名:

    func TestName(t *testing.T) {

    // ...

    }

  • 測試函數的名字必須以Test開頭,可選的後綴名必須以大寫字母開頭:

    func TestSin(t testing.T) { / ... */ }

    func TestCos(t testing.T) { / ... */ }

    func TestLog(t testing.T) { / ... */ }

  • 將測試文件和源碼放在相同目錄下,並將名字命名爲{source_filename}_test.go

假設被測試文件example.go,那麼在example.go相同目錄下建立一個example_test.go的文件去測試example.go文件裏的方法。

  • 通常情況下,將測試文件和源碼放在同一個包內。

當運行go test命令時,go test會遍歷所有的*_test.go中符合上述命名規則的函數,然後生成一個臨時的main包用於調用相應的測試函數,然後構建並運行、報告測試結果,最後清理測試中生成的臨時文件。

瞭解了基礎知識,我們舉個例子來直觀瞭解在go項目中編寫單元測試的全過程,下圖是一個項目src/utils包下源碼的結構:

 

對於每個包下的每一個文件都有相應的test文件。對於slice.go這個文件,源碼是這樣的:

 

StringInSlice這個函數去檢查一個字符串是否在一個字符串列表中,輸入一個string,返回一個布爾值。

slice_test.go中對StringInSlice的測試用例是這樣的:

 

然後運行測試,運行方式有多種:

  • 當只想測試slice_test.go文件時, 使用命令:
# -v是顯示出詳細的測試結果, -cover 顯示出執行的測試用例的測試覆蓋率。

go test -v -cover=true ./src/utils/slice_test.go ./src/utils/slice.go

執行結果:

 

  • 當測試整個utils包時,使用命令:

    go test -v -cover=true ./src/utils/...
    
  • 當測試單個測試用例時,使用命令:

#./src/utils爲包utils的路徑
go test -v -cover=true ./src/utils -run TestSuccessStringInSlice

2. 自動生成表格驅動的測試用例

在go語言中表格驅動測試非常常見。表格驅動的測試用例是在表格中預先定義好輸入,期望的輸出,和測試失敗的描述信息,

然後循環表格調用被測試的方法,根據輸入判斷輸出是否與期望輸出一致,不一致時則測試失敗, 返回錯誤的描述信息。

這種方法易於覆蓋各種測試分支 ,測試邏輯代碼沒有冗餘,開發人員只需要向表格添加新的測試數據即可。

對於適用於表格驅動測試的源碼,我們採用開源工具gotests來自動生成測試用例。拿src/utils/slice.go爲例,開發環境安裝gotests,

然後運行gotests -all -w slice.go, slice_test.go會自動創建在當前目錄下,並自動生成測試代碼:

 

開發人員只需要將不同的測試數據按照tests定義的結構寫在//TODO:Add test cases下面,測試用例就完成了。

3. mock的使用實踐

mock是單元測試中常用的一種測試手法,mock對象被定義,並能夠替換掉真實的對象被測試的函數所調用。

而mock對象可以被開發人員很靈活的指定傳入參數,調用次數,返回值和執行動作,來滿足測試的各種情景假設。

那什麼情況下需要使用mock呢?一般來說分這幾種情況:

  1. 依賴的服務返回不確定的結果,如獲取當前時間。
  2. 依賴的服務返回狀態中有的難以重建或復現,比如模擬網絡錯誤。
  3. 依賴的服務搭建環境代價高,速度慢,需要一定的成本,比如數據庫,web服務
  4. 依賴的服務行爲多變。

爲了保證測試的輕量以及開發人員對測試數據的掌控,採用mock來斬斷被測試代碼中的依賴不失爲一種好方法。

每種編程語言根據語言特點其所採用的mock實現有所不同。

在go語言中,mock一般通過兩種方法來實現,一種是依賴注入,一種是通過interface,下面我們分別通過例子來說明這兩種技術實踐。

3.1 依賴注入

依賴注入爲一個類或者函數A,用到內部對象B,B在A的外部創建,當運行A調用B時,通過某種方式將外部創建的B的實例賦給A內的B。

這樣當A調用B時,B就會按照外部定義的方式去運行。下面是一個例子:

 

在測試CheckQuota時,我們看到其函數體內有一個依賴notifyUser, notifyUser是用來向用戶發送email信息,

在測試時,我們當然不希望發送真實的郵件, 因此,需要創建一個僞郵件發送函數替代真實的郵件發送函數。

 

上圖中TestCheckQuotaNotifiesUser中定義的匿名函數就是一個僞郵件發送函數,用戶自定義僞郵件函數的行爲,然後替換真實的郵件發送函數。

在這裏需要特別注意的是,在notifyUser被僞郵件函數賦值前,需要將原來的值存下來,測試用例執行完之後再賦回去, 否則notifyUser的行爲將會全局改變。

關於示例代碼的詳情請參考go語言聖經白盒測試部分。

3.2 gomock的使用

除了依賴注入,另一種mock實現是通過go語言的interface,被mock的對象需要繼承interface,並在interface中定義好被mock對象的方法。

mock對象通過實現interface的所有方法來表明自己實現了這個interface,這樣mock對象的值就可以替換被mock對象的值。

對於mock對象我們可以自己定義實現,也可以通過工具實現。開源軟件gomock3可以根據指定的interface自動生成mock對象,
並對mock對象自定義行爲和返回結果,檢查被調用次數,是一款非常好用的工具。

下面通過一個簡單的示例來描述如何使用gomock工具。

  1. 首先從github上獲取gomock的相關源碼包,並將其放在項目的vendor目錄中。

    go get github.com/golang/mock/gomock
    go get github.com/golang/mock/mockgen
    
  2. 將需要mock的方法放在interface中,使用mockgen命令指定接口實現mock接口,命令爲:

    mockgen -source {source_file}.go -destination {dest_file}.go
    
  3. 之後就可以初始化並使用dest_file.go裏生成的mock接口來自定義被mock示例調用的方法的行爲。

 

看圖中的代碼,appControllerInterface被gomock實現,生成mock對象mockControllerImpl來替代mockAppImpl.

mockControllerImpl對Applications方法的定義是不管參數是何值,當Applications被調用時會返回nil, nil,並且此方法只能被調用1次。

當mockAppImpl的Applications方法被調用時,gomock會根據預先定義的行爲給出返回值,做出判斷。

對於gomock的詳細使用情況,可參考源碼設計gomock

4. 雲平臺產品測試實踐

這裏有個關鍵詞:雲平臺, 大家會疑惑: 雲平臺的測試和其他產品的測試有什麼不同嗎?

雲平臺的產品有這樣的特點,其底層依賴的基礎服務會比較多,且難以mock。

比如本公司的開源產品crane和swan, crane是基於docker swarm實現的容器管理工具,其底層緊密的依賴docker容器。

swan是基於mesos集羣的調度器,其底層會緊密依賴mesos,同樣由於基於docker 容器技術,也會緊密依賴docker容器來實現調度。

對於調用底層docker接口的代碼,在源碼結構設計上,可以將這一部分代碼封裝成自定義的dockerclient, dockerclient繼承interface,
這樣dockerclient可以通過mock被自定義的dockerclient替代,從而斬斷依賴, 測試上層邏輯代碼。

那麼如何測試dockerclient呢, dockerclient的代碼直接調用了docker endpoint的api,而docker對象並沒有辦法mock,我們採用的方法是創建mock server。

通過定義request url, request method 來自定義返回的response status code和body。

爲了提高編寫測試用例的效率,團隊寫了一個通用的mock-server工具來供大家使用。

mock-server的優點是支持多種形式的request body和response body的數據定義, 支持的形式有string, interface, json,file, io.Reader,

這樣開發人員在定義body的數據時,可以選擇自己熟悉便利的方式來定義。

例如crane中定義docker endpoint服務來測試ListContainersID方法的代碼如下:

 

由此可看到,mock-server在解決reset api的依賴中是非常便利的。

5. CI搭建和代碼覆蓋率

在團隊認識到單元測試是控制產品質量和促進良好代碼結構的重要手段後,我們開始把單元測試代碼覆蓋率作爲代碼提交的第一道審查防線。

開發提交的pr通過github webhook觸發jenkins CI job,運行go test,查看新的更改是否能通過單元測試,並且能獲得當前每個文件的單元測試覆蓋率。

當然在github中也可以通過travis CI來實現這樣的控制。我們可以把運行測試和獲取單元測試覆蓋率的命令在Makefile中實現, 然後根據需要搭建CI環境。

 

在makefile中,需要注意的是go test是按package計算測試覆蓋率的,如果想獲得整個項目的覆蓋率,首先需要列出項目內的packages,然後遍歷packages,通過聚合統計文件coverage.out,最後算出整個項目的覆蓋率。

以上,就是數人云做雲平臺下Go語言單元測試實踐:)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章