DAY8 — 讓你的 Backend 萬物皆虛,萬事皆可測 — Clean Architecture 測試篇
被選召的 Gopher 們,從零開始探索 Golang, Istio, K8s 數碼微服務世界 — 第12屆iT邦幫忙鐵人賽
本文章同時發佈於:
文章為自己的經驗與夥伴整理的內容,設計沒有標準答案,如有可以改進的地方,請告訴我,我會盡我所能的修改,謝謝大家~
大家好,繼昨天DAY07的介紹後,Clean Architecture 的威力大家已經見識到了,我們有著獨立可替換的彈性架構
,那還有沒有什麼優點呢?有的,就是:
高可測試(Testability)性
一個簡單的測試
你可能像我一樣曾經寫過這樣的程式碼:
如果有天要 unit test result的演算
,必須要使nameService.GetName()
會回傳固定值,而不是真的打到外部 API。
因為這樣如果測試出錯,我們才能精確的說是演算錯誤了,而非外部 API 出事了。
使nameService.GetName()
回傳固定值的做法稱為mock
,就是將nameService.GetName()
替換成一個會回傳固定值的替身
。
但剛剛得程式碼,我們根本沒有機會進行替換動作,因為nameService
在GetName
裡頭產生,其實我們只要做一個簡單的小動作,就可以避免這個事情發生,就是:
將所需的變數、實體、函示從外部帶入
那麼我們就可以從外部做一個假的nameService
丟入了,
摁…這不就單純只是不要把東西寫死在 function 中嗎?
對,其實這就是依賴注入(DI)的精神之一,只不過為了要避免爆炸,所以又需要 interface 去告訴不同 caller,此參數有哪些 function,這導致其他的理論產生。
如果是這樣的話那我不就每層都可以測試了?!
是的,因為:
Clean Architecture 每層都用依賴注入(DI),所以你每層都可以換成 Mock 實體來測試
接下來我們將說明每層測試的重點
與第一次測試可能會困惑的地方
。
以下的 code 全部都在Github-DAY07,可以直接 clone 下來參考對照會比較有感覺。
Repository 層
測試重點: 呼叫外部物件的方式是否正確,並非測試外部物件
講解1 - go-sqlmock製作一個假的mock DB
: 如重點所說,我們並不關心外部 DB 的實際狀況,我們只關心組出來的SQL command
是否正確,所以可以用 go-sqlmock 做一個假的 DB。講解2 - 產生在mock DB需要的假資料
: 既然要測試的是GetByID
的邏輯,那就必須先產生一組假的資料,讓GetByID
真的有資料可以撈取。講解3 - 將假資料轉換成row的struct
:講解2
產生的資料是domain.Digimon
的 data struct,我們必須把它轉換成 row 的 data struct,才會符合真實對 DB 取 row 的狀況。講解4 - 預定mock DB會接收到何種SQL command
: 這邊是重點,我們還必須預定好會回傳的值,mock DB 在接收到正確的 command 就會回傳預定的回傳值。講解5 - 把 mock DB 丟入後 Repository 層,驗證結果是否正確
第一次測試時你可能會困惑:「我都把 DB 做成假的了,那我在測試什麼?」,答案是SQL command是否組對
。
不論你用 gorm, SQL command,當傳進來的參數進行一連串運算後,最後一定會變為一個 SQL command,所以先設定好 mock DB要接受什麼樣的command
與接收正確之後要回傳什麼
是測試目的,並且最後可以用回傳的值來驗證是否是我們一開始設定好的來驗證。
Usecase 層
測試重點: 業務邏輯路線是否預期,並非測試 Repository 層
Usecase 層沒有像 go-sqlmock 這樣通用的 mock 套件可以使用,但是還記得有 Domain 層定好的 interface 嗎?
我們可以使用mockery,依照定好的interface生產出mock物件
,依照mockery安裝好後:
此時會多出 mocks 資料夾,這就是mock data struct
講解1 - 透過mocks資料夾產生mockDigimonRepo
講解2 - 依照不同情境區分測試
: Usecase 層比較會有不同的程式路線
產生,例如:「數碼蛋參數如果沒帶的話、如果 DB 爆掉的話等等」,所以我們可以依照這些情境來區分測試講解3 - 設定mockDigimonRepo預定接收的參數與回傳的資料
: 這邊是最有趣的部分,你可以透過.On()
來定義 mockDigimonRepo 在 Usecase 層裡被呼叫的時候預定要接收什麼,並且回傳資料供邏輯繼續使用。講解4 - 將mockDigimonRepo丟入Usecase層並實際運行
。講解5 - 驗證Usecase回傳資料與mockDigimonRepo運行的結果
:.AssertExpectations
要稍微注意一下,他是在驗證.On()
是否真的有被 call 到。
在講解3
處,有著強大的功能:
.On()
: 預期要呼叫什麼 function。mock.MatchedBy()
: 預期參數要長什麼樣子。.Return()
: 指定回傳值。.Once()
: 將此.On
效果只作用一次,這可以使你設定同個function
在不同次的.On
都有不同結果。.Run()
: 雖然沒有出現在此測試,但非常有用。Golang 有者許多bind(&body)
的指標綁定資料方式,這不是一進一出的方式,所以無法用.Return()
來指定結果。而.Run()
可以捕捉bind(&body)
中的body
指標位置,並且修改其內容,以達到指定結果的效果
這使業務邏輯所有的路線都可測試到
,真的是非常有趣 XD。
Delivery 層
測試重點: Delivery 層是否都正確接收了引擎的參數,並非測試 Usecase 層
Delivery 層與 Usecase 層概念相同(事實上每層都相同),都是創造上一層的 mock 實體,並專注測試此層的邏輯。
要注意的地方是,此測試並非真的起了一個 Server 以外部打 API 近來的方式測試。
而是在講解1
處之前設定好了 router 後,透過httptest.NewRecorder()
產生一個 request,以r.ServeHTTP(w, req)
的方式丟入 Golang-Gin 的引擎中。
好用的測試指令
一次跑全部專案的測試,你可以把此指令放上 CI,這樣就可以在每次 CD 之前來測試一下!
$ cd go-server
$ go test ./...
(cmd/main.go:31:3
可以不必理會,那是因為讀取.env
失敗的問題,但跟測試無關)
但是否我們程式中有沒測到的路線
呢?這也是可以靠 Golang 自動偵測的,運行以下指令:
最後會開啟此 html file,
可以看到上方可以顯示測試覆蓋率(就是路線覆蓋率),而下方會顯示沒測試到的 code,太神奇啦!
參考
- bxcodec/go-clean-arch
- Trying Clean Architecture on Golang
- unit testing — How to measure code coverage in Golang? — Stack Overflow
- testing — How to
go test
all tests in my project? - Stack Overflow - unit testing — Mock interface method twice with different input and output using testify — Stack Overflow
- Using testify for Golang tests — ncona.com — Learning about computers
Write Medium in Markdown? Try Markdium!