乾貨—Go語言編寫單元測試
今天是元宵節,祝所有知乎的「攻城獅」們節日快樂,今天給大家分享的文章是測試中很重要的書面表達——測試用例的模板套用,希望在今後的一年中大家都能發發發~
Go 語言對於單元測試是很重視的,且不說其他的作者的背景、開源庫、第三方的支持之類的,有兩點讓我對 Go 語言關於單元測試的重視程度的有信心的點在於:
1.Go 語言源代碼和內置庫自身的單元測試完備性
2.Go 語言自帶單元測試命令
從這兩點,我認為測試在 Go 語言中具有非常重要的地位,所以在這篇文章中,我也嘗試講一些關於 Go 語言單元測試的東西。
編寫 Go 單元測試代碼
Go 的測試方法看上去相對比較低級,它依賴於命令 go test 和一些能用 go test 運行的測試函數的編寫約定。但是,我認為這就是所謂的 Go 風格,用 Go 以來,我的感受是 Go 語言就是保持了 C 語言編程習慣的一門語言。
首先,為了開始這篇文章,我寫一個簡單的函數用作後面要測試的例子,但是,考慮到後面可能要講一些稍微複雜一點的內容,所以,這個例子我留有一些可以改變的地方,大家可以選擇著看:
這個例子就是這麼簡單,將這個文件命名為 main.go,然後我們就應該編寫測試代碼了。測試代碼的文件放置的位置可以隨意,package 也可以隨意寫,但是,文件名必須以 _test 結尾,所以,我這裡就命名為 main_test.go。
這裡編寫測試函數,有幾個需要注意的點:
1.每個測試文件必須以 _test.go 結尾,不然 go test 不能發現測試文件
2.每個測試文件必須導入 testing 包
3.功能測試函數必須以 Test 開頭,然後一般接測試函數的名字,這個不強求
根據這些條件,我們可以寫出一個測試文件:
測試文件寫完之後,我們就應該執行測試了,打開命令行工具,敲入這條命令:go test main_test.go main.go -v -cover
然後就應該等待測試結果了,這裡加了兩個參數,分別是 -v 和 -cover,如果不加上的話你會發現只有 Test Pass 的簡單提示,而看不到我們加了參數的具體提示:
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
coverage: 50.0% of statements
ok command-line-arguments 0.008s
基於表的測試方式
在 Go 語言中,有一種常用的測試套路,叫做基於表的測試方式,其核心就是我們需要針對不同的場景,其實也就是不同的輸入和輸出來驗證一個功能。例如我們要驗證的 Add 函數,我們需要驗證的功能點有很多,例如:
●兩個正數相加是否正確
●兩個負數相加是否正確
●一個正數加上一個負數是否正確
●有一個數為 0 是否正確
那麼,我們就可以使用 基於表的測試方式 了,代碼可以這樣寫:
Mock 依賴
前面介紹的測試都是比較簡單的,功能簡單的話我們就可以直接給定輸入,然後看輸出是否符合預期,這樣就可以很簡單得寫完單元測試了。但是,有的時候,由於業務邏輯的複雜性,功能代碼並不會就這麼直接,往往還會摻雜很多其他組件,這就給我們的測試工作帶來很大的麻煩,我這裡列舉幾個常見的依賴:
●組件依賴
●函數依賴
組件依賴和函數依賴是兩種比較常見的依賴,但是,這兩種依賴也是可以擴展開來說的,既可能來自於我們自己編寫的組件/函數,也可能是引入其他人寫的。但是,無妨,對於這些情況,我們都會做一些分析。
組件依賴處理
使用 Go 語言開發項目的時候,我們應該經常會抽離組件模塊的,既然抽離了組件和模塊,那麼久離不開組件的依賴了,既然有依賴,在測試的時候我們很多時候都是希望屏蔽掉依賴組件的影響,從而更好得測試現有代碼的細節;或者說,我們希望根據自己的測試目標,控制依賴組件的行為。但是,如果我們還想簡單得通過控制輸入和輸出來控制依賴組件的行為,這個難度還是比較大的,所以,在這種情況下,我們一般會考慮傳一個 Stub 組件進入,從而達到控制依賴組件行為的效果。
舉一個例子先,例如我們比較常見的 Service 層和 DAO 層的操作,Service 處理完邏輯之後,交給 DAO 層進行持久化,或者需要調用 DAO 層從持久化中獲取一些必要的數據;在測試的時候,我們很多時候不希望真的持久化或者從持久化中獲取數據,那麼就會對 DAO 層進行一些 Mock。首先,先給出一個運行的例子:
這裡我們想要測試 Service 的正確性,但是又不想要真的持久化 DAO,所以,這個時候我們會自己創建一個 Stub,然後提供給 Service,同時,我們還能操作 DAO 的行為,達到運行得效果,例如:
這樣我們就能測試到登錄成功的代碼了,登錄成功的測試了,我們還希望測試一下登錄失敗的,那麼也很簡單,修改一下就可以了唄:
這裡對測試代碼稍微改了一下,可以發現,我們可以通過修改一個變數來控制 Stub 的輸出,從而達到測試不同功能的效果,這就解決了組件依賴的問題。
函數依賴
函數依賴相比於組件依賴會更麻煩一點,因為我們在前面可以看到,組件依賴的話我們可以傳遞 Stub 進行,這樣我們可以隨意得控制 Stub 的行為,但是函數不行呀,這裡我們又不能傳函數進去,因為函數是被 import 進去的啊。問題就在這了,因為函數是被 import 進去的,所以可以理解為函數是全局的了,既然這樣,那麼我們為什麼不修改一下函數呢?什麼意思?我們先來看著正常的業務例子:
這裡我是想表達的一個意思就是需要先登錄,然後登錄完之後我們才能回復消息,這裡我們的登錄邏輯是簡單的,但是,在實際業務中可能這裡的登錄邏輯就設計到 DB 訪問等等,我們希望不走真實的邏輯,而是自己來控制 Login 的行為。
首先,先分析一下我們的 UT 目的,我們的目的是測試 Reply 函數,我們期望是 Login 成功,那麼 Reply 也應該是成功的;如果 Login 失敗,那麼 Reply 也應該是失敗的。這個測試結論不應該被 Login 所影響,及時以後 Login 邏輯修改了,我們也應該是這個邏輯,不會受到影響,那麼我們可以這麼編寫 UT:
這裡可以發現,我們是修改了 Login 這個函數的代碼,從而控制 Login 函數的返回值,這樣我們就可以測試我們寫的代碼的邏輯是否正確了。
可能有心細的同學發現,第二個函數依賴裡面的代碼有個特別的地方,在於函數 Login 是個變數,為什麼要用函數變數呢,不能直接使用函數嗎?這裡確實是一個麻煩得地方,因為如果使用直接函數的話,我們沒得賦值,那麼也就無法修改它了。如果代碼不是我們自己寫的,而是使用的其他同事的代碼,那麼問題就大了。這種情況下,我們可以怎麼處理呢?
在 Reference 的中有一篇文章:Mocking functions in Go 介紹了一種方法,那就是將我們引用的函數賦值為函數變數再使用,從而達到同樣的效果。
第三方庫
在前面的介紹中,我們都是自己重新寫了一個 Stub 類,但是同時問題暴露了,寫起來比較複雜和繁瑣,所以就有了一些第三方庫可以方便我們編寫這些東西,Gomock 就是一個,這裡就簡單演示一下 GoMock 的一些功能。
gomock 有兩個組件,分別是 gomock 和 mockgen。mockgen 可以根據我們的 interface 生成對應的 Stub 對象,例如我們前面提到的 DAO,因為我們 DAO 的 Interface 已經寫好了,所以我們可以很方便得生成 StubDao,只需要使用命令:
$ mockgen -source=main.go > mock_dao.go
然後我們查看一下 mock_dao.go 的內容:
可以發現兩個函數都被實現了 ReadAll 和 SaveData,但是,同時我們也發現,生成的 DAO 比我們預期的要複雜得多,至於為什麼葯這麼複雜,看看接下來的使用就明白了。現在我們已經有了 DAO,那麼下一步就是控制 DAO 的輸出了。
這裡可以看到,使用 gomock 可以讓我們的控制輸出更加簡便,之前我們需要通過控制變數的方式來達到控制輸出的目的,但是這裡可以很方便得使用:
d.EXPECT().FuncXXX.Return(xxxx)
來指定函數應該返回什麼結果,確實方便了,同時,還省去了我們自己編寫 MockClass 的時間,這個還是值得一試的。
總結
好啦,本文關於 Go 的 UT 就差不多這麼多了,這裡介紹了 Go 單元測試的編寫套路,以及介紹了我們在項目中常見的一些場景的處理,最後再介紹了一款第三庫用於加快 UT 的編寫。希望,Go 語言相對簡單的編寫套路和模式能夠讓大家對 UT 有所重視,並且願意寫起來。
如想了解更多軟體測試技術,請前往51Testing軟體測試網-中國軟體測試人的精神家園,學習更多測試方法~
推薦閱讀: