1. Go對單元測試的原生支持
1.1 testing——Go內置的單元測試庫。
要編寫一個新的測試,需要創建一個以 _test.go 結尾的文件,該文件包含 TestXxx 函數。 將該文件放在與被測試的包相同的包中。
通過 go test 命令,能夠自動執行如下形式的任何函數:
func TestXxx(*testing.T)
注意:Xxx 可以是任何字母數字字符串,但是第一個字母不能是小寫字母(一般接被測試函數名字,不強求)。傳遞給測試函數的參數是 *testing.T 類型。它用於管理測試狀態並支持格式化測試日志。測試日志會在執行測試的過程中不斷累積,並在測試完成時轉儲至標准輸出。
詳情參見:The-Golang-Standard-Library-by-Example
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.1.html
1.2 TestMain
在寫測試時,有時需要在測試之前或之后進行額外的設置(setup)或拆卸(teardown);有時,測試還需要控制在主線程上運行的代碼。為了支持這些需求,testing 提供了 TestMain 函數:
func TestMain(m *testing.M)
如果測試文件中包含該函數,那么生成的測試將調用 TestMain(m),而不是直接運行測試。
TestMain 運行在主 goroutine 中, 可以在調用 m.Run 前后做任何設置和拆卸。注意,在 TestMain 函數的最后,應該使用 m.Run 的返回值作為參數調用 os.Exit。
詳情參見:TestMain
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.5.html#testmain
1.3 httptest——HTTP測試輔助工具
Go 標准庫專門提供了 httptest 包專門用於進行 http Web 開發測試。
httptest包最關鍵的是提供了一個 http.ReponseWriter接口的實現結構:httptest.ReponseRecorder,通過它可以得到一個http.ReponseWriter,並以此來接收服務器返回的響應包。
詳情參見:httptest - HTTP 測試輔助工具
https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter09/09.6.html
1.4 測試覆蓋率
Go 從 1.2 開始,引入了測試覆蓋率的支持,使用的是 cover 相關的工具(go test -cover、go tool cover)。
詳情參見:The cover story
2. 斷言庫
Go標准包里並沒有斷言庫,但是不使用斷言庫進行結果校驗的話,測試代碼將會變得非常臃腫,可讀性和可維護性都會很差。不過好在有第三方框架可以讓我們使用。
2.1 testify
github地址:https://github.com/stretchr/testify
特性:
- 在提供斷言功能之外,還提供了mock的功能
- suite包可以給每個測試用例進行前置操作和后置操作的功能(例如初始化和清空數據庫)
2.2 gocheck
godoc地址:https://godoc.org/gopkg.in/check.v1
特性:
- 豐富了單元測試常用的 assert 斷言,判斷動詞deep multi-type 對比,字符串比較以及正則匹配
- 測試用例組織集合方面按suite組織測試用例,支持suite級別的 setup() 和 teardown()
- 對於臨時文件支持創建、刪除臨時文件和目錄。
詳情參見:gocheck 使用介紹
2.3 goconvey
github地址: https://github.com/smartystreets/goconvey
特性:
- 直接集成go test
- 可以管理和運行測試用例
- 提供了豐富的斷言函數
- 支持很多 Web 界面特性(通過http://localhost:8080訪問)
- 設置界面主題
- 查看完整的測試結果
- 使用瀏覽器提醒
- 自動檢測代碼變動並編譯測試
- 半自動化書寫測試用例:http://localhost:8080/composer.html
- 查看測試覆蓋率:http://localhost:8080/reports/
- 臨時屏蔽某個包的編譯測試
詳情參見:GoConvey框架使用指南
2.4 比較
其實gocheck我沒怎么用過,只是當時調研的時候看到了,在斷言方面看起來和其他的差不多。
testify和goconvey都有嘗試,最后采用的是goconvey,所以對goconvey更熟悉一點。
- testify
- star數和活躍度較高:這個其實挺重要的,因為很多人都在用這個框架的話,這個框架會得到更好的完善和發展,也會更有生命力。在使用時碰到的問題也可以很方便的在issue中找到答案;
- testify類似於gocheck和gomock的結合體,但是其mock使用並不是很方便,所以建議還是使用專門的mock框架。
- goconvey
- 可以管理和運行測試用例,通過嵌套來體現測試用例之間的關系。這是我當時選擇使用goconvey的一個很重要原因,它可以將測試代碼組織得更富邏輯性和結構化,提高了測試代碼的可讀性和可維護性
- 支持在web界面進行自動化編譯測試。之前在油管上看到一個博主通過web界面來半自動化生成測試代碼。不過我實際並沒有使用這個功能,感覺這個功能適合邏輯簡單/清楚的代碼。
3 mock/stub方案
3.1 識別依賴
普遍來說,我們遇到最常見的依賴無非下面幾種:
- 網絡依賴——函數執行依賴於網絡請求,比如第三方http-api,rpc服務,消息隊列等
- 數據庫依賴
- I/O依賴(文件)
- 還未開發完成的功能模塊
3.2 mock和stub的區別
這個話題也算是老生常談了。幾句話很難解釋清楚,有興趣可以閱讀Martin Fowler的文章。
stub本質上是對真實對象的一個模擬,比如調用者需要一個值,那就讓stub輸出一個值,如果調用者需要傳遞一個值給stub,那就在stub中定義一個方法接受該參數,相當於“依賴部分”的一個簡化實現。mock則是在程序代碼中向被測試代碼注入“依賴部分”,模擬出函數調用返回的結果。
個人認為兩者最大的區別在於依賴對象是否和被測對象有交互,從結果來看,stub不會使測試失敗,它只是為被測對象提供依賴的對象,並不改變測試結果,而mock則會根據不同的交互測試要求,很可能會更改測試的結果。stub是state-based,關注的是輸入和輸出。mock是interaction-based,關注的是交互過程。
mock和stub還有一個重要的區別就是expectiation。對於mock來說,expectiation是重中之重:我們期待方法有沒有被調用,期待適當的參數,期待調用的次數,甚至期待多個mock之間的調用順序。所有的一切期待都是事先准備好,在測試過程中和測試結束后驗證是否和預期的一致。而對於stub,通常都不會關注expectiation,沒有任何代碼來幫助判斷這個stub類是否被調用。雖然理論上某些stub實現也可以通過自己編碼的方式增加對expectiation的內容,比如增加一個計數器,每次調用+1之類,但是實際上極少這樣做。
在Go中,如果要用stub,那將是侵入式的,必須將代碼設計成可以用stub方法替換的形式。為了測試,需要專門用一個全局變量 來保存具有外部依賴的方法。然而在不提倡使用全局變量的Go語言當中,這顯然是不合適的。所以,並不提倡這種Stub方式。
但其實這兩種方法並不是割裂的,例如像下文提到的gomock框架除了像其名字一樣可以mock對象以外,還提供了stub的功能。軟件工程沒有銀彈,我們需要根據合適的場景選用合適的方法,甚至可以結合多種方法使用。
詳情參見:Mocks Aren't Stubs(Martin Fowler)
https://martinfowler.com/articles/mocksArentStubs.html
以及 中文翻譯
3.3 gostub
github地址:https://github.com/prashantv/gostub
特性:
- 可以為全局變量、函數、過程打樁
- 比gomock輕量,不需要依賴接口
缺陷:
- 對項目源代碼有侵入性,即被打樁方法必須賦值給一個變量,只有以這種形式定義的方法才能別打樁
詳情參見:GoStub框架使用指南
3.4 gomock
github地址:https://github.com/golang/mock
特性:
- golang官方開發維護的接口級別的mock方案
- 包含了GoMock包和mockgen工具兩部分,其中GoMock包完成對樁對象生命周期的管理,mockgen工具用來生成interface對應的Mock類源文件。
缺陷:
- 只有以接口定義的方法才能mock
- 需要用mockgen生成源文件,然后用gomock去實現自己想要的數據,用法稍重。
詳情參見:使用Golang的官方mock工具—gomock
https://www.jianshu.com/p/598a11bbdafb
和 GoMock框架使用指南
3.5 gomonkey
github地址:https://github.com/bouk/monkey
特性:
- 可以為全局變量、函數、過程、方法打樁,同時避免了gostub對代碼的侵入
缺陷:
- 對inline函數打樁無效
- 不支持多次調用樁函數(方法)而呈現不同行為的復雜情況
詳情參見:Monkey框架使用指南
3.6 sqlmock
github地址: https://github.com/DATA-DOG/go-sqlmock
特性:
- 適用於和數據庫的交互場景。可以創建模擬連接,編寫原生sql 語句,編寫返回值或者錯誤信息並判斷執行結果和預設的返回值
- 提供了完整的事務的執行測試框架,支持prepare參數化提交和執行的Mock方案
- 持久層框架底層一般都使用”github.com/go-sql-driver/mysql”,所以一般都能夠使用sqlmock庫進行mock
缺陷:
- 因為是正則匹配,所以可能漏掉sql的語法錯誤
- 寫入后沒法驗證
3.7 httpexpect
github地址:https://github.com/gavv/httpexpect
特性:
- 適用於對http的clent進行測試,對服務端的回包進行打樁
- 支持對不同方法(get,post,head等)的構造,支持自定義返回值json
sqlmock和httpexpect都蠻簡單的,看完github主頁的QuickStart基本就會用了~~
4 使用goconvey+gomonkey+sqlmock進行測試
4.1 選擇原因
- 外層框架——goconvey。項目代碼很多邏輯比較復雜,需要編寫不同情況下的測試用例,用goconvey組織的測試代碼邏輯層次比較清晰,有着較好的可讀性和可維護性。斷言方面感覺convey和testify功能差不多。不過convey沒有testify社區活躍度高,后續使用convey時碰到一些問題,都不太容易找到解決辦法,給作者提issue,感覺回復效率也不是很高。
- 函數mock——gomonkey。項目代碼基本都不是基於interface實現的,所以不太方便使用gomock,項目目前運行穩定,所以也不想因為單元測試重構原來的代碼,所以也不太方便gostub。好在還有gomonkey可以用,基本符合我們對函數打樁的需求。
- 持久層mock——sqlmock。我們持久層的框架是gorm。當時考慮2種方法進行mock,一種是使用gomonkey對gorm的函數進行mock,另一種則是選用sqlmock。但碰到下圖所示的sql語句,如果使用gomonkey的話需要對連續調用的gorm函數都進行mock,過於繁雜。而用sqlmock的話只需匹配對應的sql語句即可。
newDB = MysqlDB.ModelTable(c, &Basexxx{}, c.AppID()).Where("type = ?", libType).Limit(limit).Offset(offset).Order("created_at desc").Find(&libxxxs)
4.2 gorm+sqlmock使用方法
初始化sqlmock后,然后使用dialect和dsn打開一個新的gorm連接並賦值給數據庫操作實例
_, mock, _ = sqlmock.NewWithDSN("sqlmock_db")
MysqlDB.DB, _ = gorm.Open("sqlmock", "sqlmock_db")
接下來就和sqlmock的普通使用沒什么區別了,只要mock時能夠成功的匹配gorm生成的sql語句即可
詳情參見:Stub database connection with GORM
https://blog.valletta.io/blog/2018-07-05-stub-database-connection-with-gorm/
4.3 踩坑記錄(持續更新~)
- 問題描述:
- 測試函數在run的時候fail,在無斷點debug的時候pass。被patch的函數是下例中的函數A。
func A(arg string) error {
return B(arg)
}
原因:
- run的時候會做編譯器優化,調用A會直接被優化為調用B(內聯)。所以對前者的patch並沒有成功。
在不改動原有代碼的情況下,有2種解決方案:
- 給函數B也打補丁
- 在go test時加參數來避免編譯器優化內聯 go test -gcflags=-l
5 其他
5.1 單元測試的粒度
對於剛開始做單元測試的同學來說,如何把握單元測試的粒度是一個讓人頭疼的問題。
測試粒度做的太細,會耗費大量的開發以及維護時間,每改一個方法,都要改動其對應的測試方法。當發生代碼重構的時候那簡直就是噩夢(因為所有的單元測試又都要寫一遍了…)。
如果單元測試粒度太粗,一個測試方法測試了n多方法,那么單元測試將顯的非常臃腫,脫離了單元測試的本意,容易把單元測試寫成集成測試。
5.2 單元測試的成本和收益
在受益於單元測試的好處的同時,也必然增加了代碼量以及維護成本。
下面這張成本/價值象限圖清晰闡述了在不同性質的系統中單元測試的成本和價值之間的關系。

- 依賴很少的簡單代碼(左下)
- 對於外部依賴少,代碼又簡單的代碼。自然其成本和價值都是比較低的。
- 例如Go官方庫里errors包,整個包就兩個方法 New()和 Error(),沒有任何外部依賴,代碼也很簡單,所以其單元測試起來也是相當方便。
- 依賴較多的簡單代碼(右下)
- 依賴一多,mock和stub就必然增多,單元測試的成本也就隨之增加。但代碼又如此簡單,這個時候寫單元測試的成本已經大於其價值,還不如不寫單元測試。
- 依賴很少的復雜代碼 (左上)
- 像這一類代碼,是最有價值寫單元測試的。比如一些獨立的復雜算法(銀行利息計算,保險費率計算,TCP協議解析等),像這一類代碼外部依賴很少,但卻很容易出錯,如果沒有單元測試,幾乎不能保證代碼質量。
- 依賴很多的復雜代碼(右上)
- 這種代碼顯然是單元測試的噩夢。寫單元測試吧,代價高昂;不寫單元測試吧,風險太高。
- 像這種代碼我們盡量在設計上將其分為兩部分:1.處理復雜的邏輯部分 2.處理依賴部分 然后1部分進行單元測試。
https://www.toutiao.com/a6715009799680950798/