Golang項目的測試實踐
最近有一個項目,鏈路涉及了4個服務。最核心的是一個配時服務。要如何對這個項目進行測試,保證輸出質量,是最近思考和實踐的重點。這篇就說下最近這個實踐的過程總結。
測試金字塔
按照Mike Cohn提出的“測試金字塔”概念,測試分為4個層次

最下面是單元測試,單元測試對代碼進行測試。再而上是集成測試,它對一個服務的接口進行測試。繼而是端到端的測試,我也稱他為鏈路測試,它負責從一個鏈路的入口輸入測試用例,驗證輸出的系統的結果。再上一層是我們最常用的UI測試,就是測試人員在UI界面上根據功能進行點擊測試。
單元測試
對於一個Golang寫的服務,單元測試已經是很方便了。我們在寫一個文件,函數的時候,可以直接在需要單元測試的文件旁邊增加一個_test.go的文件。而后直接使用 go test 直接跑測試用例就可以了。
一般單元測試,最直接的衡量標准就是代碼覆蓋率。
單元測試一般測試的對象是一個函數,一個類。
這個部分已經有很多實踐例子了,就沒什么好聊的。
集成測試
思考和需求
對於一個服務,會提供多個接口,那么,測試這些接口的表現就是集成測試最重要的目標了。只有通過了集成測試,我們的這個服務才算是有保障。
手頭這個配時項目,對外提供的是一系列HTTP服務,基本上代碼是以MVC的形式架構的。在思考對它的集成測試過程中,我希望最終能做到下面幾點:
首先,我希望我手上這個配時服務的集成測試是自動化的。最理想的情況下,我能調用一個命令,直接將所有case都跑一遍。
其次,衡量集成測試的達標指標。這個糾結過一段時間,是否需要有衡量指標呢?還是直接所有case通過就行?我們的服務,輸入比較復雜,並不是簡單的1-2個參數,是一個比較復雜的json。那么這個json的構造有各種各樣的。需要實現寫一些case,但是怎么保證我的這些case是不是有漏的呢?這里還是需要有個衡量指標的,最終我還是選擇用代碼覆蓋率來衡量我的測試達標情況,但是這個代碼覆蓋率在MVC中,我並不強制要求所有層的所有代碼都要覆蓋住,主要是針對Controller層的代碼。controller層主要是負責流程控制的,需要保證所有流程分支都能走到。
然后,我希望集成測試中有完善的測試概念,主要是TestCase, TestSuite,這里參考了JUnit的一些概念。TestCase是一個測試用例,它提供測試用例啟動和關閉時候的注入函數,TestSuite是一個測試套件,代表的是一系列類似的測試用例集合,它也帶測試套件啟動和關閉時候的注入函數。
最后,可視化需求。我希望這個測試結果很友好,能有一個可視化的測試界面,我能很方便知道哪個測試套件,哪個測試用例中的哪個斷言失敗了。
集成測試實踐
Golang 只有_test.go的測試,其中的每個Test_XXX相當於是TestCase的概念,也沒有提供測試case啟動,關閉執行的注入函數,也沒有TestSuite的概念。首先我需要使用 Golang 的test搭建一個測試架子。
集成測試和單元測試不一樣,它不屬於某個文件,集成測試可能涉及到多個文件中多個接口的測試,所以它需要有一個單獨的文件夾。它的目錄結構我是這么設計的:

suites
存放測試套件
suites/xxx
這里存放測試套件,測試套件文件夾需要包含下列文件:
before.go存放有
- SetUp() 函數,這個函數在Suite運行之前會運行
- Before() 函數,這個函數在所有Case運行之前運行
after.go存放有
- TearDown() 函數,這個函數在Suite運行之后會運行
- After() 函數,這個函數在Suite運行之后運行
run_test.go文件
這個文件是testsuite的入口,代碼如下:
package adapt
import "testing"
import . "github.com/smartystreets/goconvey/convey"
func TestRunSuite(t *testing.T) {
SetUp()
defer TearDown()
Convey("初始化", t, nil)
runCase(t, NormalCasePEE001)
runCase(t, PENormalCase01)
runCase(t, PENormalCase04)
runCase(t, PENormalCase11)
runCase(t, PENormalCase13)
runCase(t, PENormalCase14)
runCase(t, NormalCasePIE001)
runCase(t, NormalCasePIE002)
runCase(t, NormalCase01)
runCase(t, NormalCase02)
runCase(t, NormalCase07)
runCase(t, NormalCase08)
runCase(t, NormalCasePIN003)
runCase(t, NormalCasePIN005)
runCase(t, NormalCasePIN006)
runCase(t, NormalCasePIN015)
}
func runCase(t *testing.T, testCase func(*testing.T)) {
Before()
defer After()
testCase(t)
}
envionment
初始化測試環境的工具
當前我這里面存放了初始化環境的配置文件和db的建表文件。
report
存放報告的地址
代碼覆蓋率需要額外跑腳本
在tester目錄下運行:
sh coverage.sh 會在report下生成coverage.out和coverage.html,並自動打開瀏覽器
引入Convey
關於可視化的需求。
我引入了Convey這個項目,http://goconvey.co/ 。第一次看到這個項目,覺得這個項目的腦洞真大。
下面可了勁的誇一誇這個項目的優點:
斷言
首先它提供了基於原裝go test的斷言框架;提供了Convey和So兩個重要的關鍵字,還提供了 Shouldxxx等一系列很好用的方法。它的測試用例寫下來像是這個樣子:
package package_name
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestIntegerStuff(t *testing.T) {
Convey("Given some integer with a starting value", t, func() {
x := 1
Convey("When the integer is incremented", func() {
x++
Convey("The value should be greater by one", func() {
So(x, ShouldEqual, 2)
})
})
})
}
很清晰明了,並且超贊的是很多參數都使用函數封裝起來了,go中的 := 和 = 的問題能很好避免了。並且不要再絞盡腦汁思考tmp1,tmp2這種參數命名了。(因為都已經分散到Convey語句的func中了)
Web界面
其次,它提供了一個很贊的Web平台,這個web平台有幾個點我非常喜歡。首先它有一個case編輯器

什么叫好的測試用例實踐? 我認為這個編輯器完全體現出來了。寫一個完整的case先考慮流程和斷言,生成代碼框架,然后我們再去代碼框架中填寫具體的邏輯。這種實踐步驟很好解決了之前寫測試用例思想偷懶的問題,特別是斷言,基本不會由於偷懶而少寫。
其次它提供很贊的測試用例結果顯示頁面:

很贊吧,哪個case錯誤,哪個斷言問題,都很清楚顯示出來。
還有,goconvey能監控你運行測試用例的目錄,當目錄中有任何文件改動的時候,都會重新跑測試用例,並且提供提醒

這個真是太方便了,可以在每次保存的時候,都知道當前寫的case是否有問題,能直接提高測試用例編寫的效率。
TestSuite初始化
Web服務測試的環境是個很大問題。特別是DB依賴,這里不同的人有不同的做法。有使用model mock的,有使用db的。這里我的經驗是:集成測試盡量使用真是DB,但是這個DB應該是私有的,不應該是多個人共用一個DB。
所以我的做法,把需要初始化的DB結構使用sql文件導出,放在目錄中。這樣,每個人想要跑這一套測試用例,只需要搭建一個mysql數據庫,倒入sql文件,就可以搭建好數據庫環境了。其他的初始化數據等都在TestSuite初始化的SetUp函數中調用。
關於保存測試數據環境,我這里有個小貼士,在SetUp函數中實現 清空數據庫+初始化數據庫 ,在TearDown函數中不做任何事情。這樣如果你要單獨運行某個TestSuite,能保持最后的測試數據環境,有助於我們進行測試數據環境測試。
TestCase編寫
在集成測試環境中,TestCase編寫調用HTTP請求就是使用正常的 httptest包,其使用方式沒有什么特別的。
代碼覆蓋率
goconvey有個小問題,測試覆蓋率是根據運行goconvey的目錄計算的,不能額外設置,但是go test是提供的。所以代碼覆蓋率我還額外寫了一個shell腳本
#!/bin/bash
go test -coverpkg xxx/controllers/... -coverprofile=report/coverage.out ./...
go tool cover -html=report/coverage.out -o report/coverage.html
open report/coverage.html
主要就是使用converpkg參數,把代碼覆蓋率限制在controller層。

集成測試總結
這套搭建實踐下來,對接口的代碼測試有底很多了,也測試出不少controller層面的bug。
端到端測試
這個是測試金字塔的第二層了。
關於端到端的測試,我的理解就是全鏈路測試。從整個項目角度來看,它屬於一個架構的層次了,需要對每個服務有一定的改造和設計。這個測試需要保證的是整個鏈路流轉是按照預期的。
比如我的項目的鏈路通過了4個服務,一個請求可能在多個服務之間進行鏈路調用。但是這個項目特別的是,這些服務並不都是一個語言的。如何進行測試呢?
理想的端到端測試我的設想是這樣的,測試人員通過postman調用最上游的服務,構造不同的請求參數和case,有的case其實可能無法通到最下游,那么就需要有一個全鏈路日志監控系統,在這個系統可以看到這個請求在各個服務中的流轉情況。全鏈路日志監控系統定義了一套tag和一個traceid,要求所有服務在打日志的時候帶上這個traceid,和當前步驟的tag,日志監控系統根據這些日志,在頁面上能很好反饋出這個鏈路。
然后測試人員每個case,就根據返回的traceid,去日志中查找,並且確認鏈路中的tag是否都全齊。
關於如何在各個服務中傳遞traceid,這個很多微服務監控的項目中都已經說過了,我也是一樣的做法,在http的header頭中增加這個traceId。
關於打日志的地方,其實有很多地方都可以打日志,但是我只建議在失敗的地方+請求的地方打上tag日志,並且是由調用方進行tag日志記錄,這樣主要是能把請求和返回都記錄,方便調試,查錯等問題。
UI測試
這個目前還是讓測試人員手動進行點擊。這種方式看起來確實比較low,但是貌似也是目前大部分互聯網公司的測試方法了。
總結
這幾周主要是在集成測試方面做了一些實踐,有一些想法和思路,所以拿出來進行了分享,肯定還有很多不成熟的地方沒有考慮到,歡迎評論留言討論。
測試是一個費時費力的工作,大多數情況下,業務的迭代速度估計都不允許做很詳細的測試。但是對於復雜,重要的業務,強烈建議這四層的測試都能做到,這樣代碼上線才能有所底氣。
