下圖是我GO專案所使用的架構,該架構是根據3-tier架構,該架構可以讓後續維護人員更好維護,雖然會相對有點花時間XD

延伸閱讀:菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧

3-tier架構 (controller, service, repository)

其實3-tier架構主要就是controller、service、repository,由我來分別介紹上圖分層各自在幹嘛~然後我會以登入來舉例,讓你更好懂!

Clent:就是我們自己,以登入功能來舉例,這邊就是我們到了某個網站,當我們打好帳號密碼按下送出後。

Middleware: 這邊不一定要經過,但是可以再進入controller前進行資料前處理,像是Token認證阿等等…、以登入功能來舉例,我做了client輸入的帳號密碼是否有非法字元。

Controller:主要是處理進來的Request,出去的Response,以及分配到哪個Service層,因為service會可以拆得非常細,所以需要分配到哪個service!以登入功能來舉例,這邊會處理若帳號密碼輸入錯誤會如何回傳失敗、若成功會如何回傳成功給client端。

Service:商業邏輯都在這,老闆的商業需求都在這XD,這邊會擺最複雜的程式邏輯,以登入功能來舉例,就是要驗證輸入的密碼與資料庫已儲存的密碼是否相同。

Repository:這邊主要是與資料庫互動的語法,SQL就是在這層實施的,以登入功能來舉例,會透過這層與下一層DB說我想要撈取帳號是這個人的資料!

DB(database):這邊就是資料儲存的地方,常見的資料庫有MySQL、Postgres、MongoDB等等,以登入功能來舉例,這邊就是儲存你client端註冊時產生的資料所儲存的地方。


接下來我會介紹單元測試實際再寫登入功能的時候會要寫甚麼?我自己在寫單元測試的時候因為不知道怎麼切架構,卡了好一陣子,規劃架構才是最難的XD,當然以下只是簡單地舉例啦,實際在寫要考慮的更多XD,我會把程式碼附在下一篇文章~

Middleware

Middleware單元測試怎麼寫:用正確案例驗證如果我的帳號密碼輸入時沒有非法字元會產生什麼回應。用錯誤案例驗證如果我的帳號密碼輸入時含有非法字元會有甚麼回應?

Controller

Controller單元測試怎麼寫:用正確案例驗證如果登入成功會回傳什麼?用錯誤案例驗證如果登入失敗會回傳什麼。

Service

Service單元測試怎麼寫:用正確案例驗證如果資料庫的密碼與輸入進來的密碼一樣會回傳什麼?用錯誤案例驗證如果資料庫的密碼與輸入進來的密碼不一樣會回傳什麼?

Repository

Repository單元測試怎麼寫:用正確案例驗證如果在資料庫找到資料。用錯誤案例驗證如果在資料庫沒有找到資料。

DB

Database單元測試怎麼寫測試 :驗證真實資料庫是否能正常啟動並進行新增、刪除、修改、查詢資料。

以上就是go專案登入功能單元測試怎麼寫的規劃,看起來很簡單,但我是從零開始,很難,程式碼會附在下一篇XD

單元測試的隱藏紅利好處

接下來要即將接觸單元測試的核心,要如何達成成功案例、錯誤案例同時去run一樣的測試,又能知道錯誤錯在哪裡,畢竟這個測試可能會讓後面維護的工程師去使用,最理想的狀況就是不要去改動到原來的程式碼,也可以通過測試。這樣才是好的測試!

  • 還記得我們前一篇到的Clean Code的FIRST法則嗎?這邊引用一下Reatpeatable!因為他跟我們這邊要提到的息息相關。

Repeatable:測試應該要可以在任何環境中重複執行,減少因環境因素而產生測試失敗的問題。


講一堆,所以單元測試的好處是啥啦?它除了可以有效避免程式可能的錯誤,還有以下隱藏紅利:

1. 確保在改變程式碼結構時不會破壞現有的功能

  • 單元測試可以檢測是否引入了新的錯誤或破壞現有功能,尤其在後續人員進行開發時,也都要以原測試為基準進行開發。

2. 提高程式碼可讀性和可維護性

  • 單元測試可以定義明確的測試案例和預期行為,讓其他開發人員更容易理解代碼的用途和期望行為。

寫單元測試大原則

我自己歸納出寫單元測試跟隨以下三個方向準沒錯啦~~~

  1. 只要有if判斷出現就要寫一個測試案例。
  2. 每個測試中最好都至少要有一個正確與錯誤案例。
  3. 盡可能消除環境相依性,如連接真實資料庫。

看起來很Easy對不對,我接下來會對第一點與第二點來簡單介紹到底是甚麼意思呢?

上面圖片是我實際的service層程式碼檔案檔名叫user_service.go可以看到有四個if,其中三個是判斷是否有錯誤,如果有錯誤會根據錯誤的不同回傳指定的錯誤json字串。我們可以看到有四種不同的錯誤那我們根據錯誤就要產出向下面這樣的測試案例~

可以看到總共有四個測試案例呢,因為一定會有一個成功,三個錯誤,為了提高我的程式覆蓋率(後面會提到這是啥),我要讓每個if都有run到!

寫單元測試大原則

  1. 只要有if判斷出現就要寫一個測試案例。
  2. 每個測試中最好都至少要有一個正確與錯誤案例。
  3. 盡可能消除環境相依性,如連接真實資料庫。

那還有第三點消除環境相依性阿,那是啥意思啊??? 簡單來說我們在做測試的時候會傾向只對眼前的功能進行測試,不會想因為資料庫沒登入,docker沒開啟等外部狀況,不會想因為其他因素造成測試焦點迷失,導致我們不知道測試到底在哪個環節出錯。

進行單元測試時不會想因為資料庫沒登入,docker沒開啟等外部狀況導致測試錯誤出現。

因此我們需要偽造物件來幫忙消除環境相依性!

消除環境相依性測試小撇步:偽造物件

偽造物件不只可以消除環境相依性,他還有其他厲害的功用呢,還記得我們有提到Clean Code的First原則中的I:Indepment嗎?他與偽造物件是一樣的概念喔!

Independent:不要讓一個測試高度相依其他測試,測試的失敗會影響其他測試也跟著失敗,會很難找出問題癥結點

  1. 減少外部依賴
    • 測試的失敗會影響其他測試也跟著失敗,會很難找出問題癥結點。
  2. 簡化測試編寫
    • 測試的城市碼量更少,因為它們可以用來代替複雜的外部依賴。
  3. 提高測試速度
    • 因為它們可以快速建立和摧毀,並且不需要真實的外部資源。
  4. 測試更加全面
    • 可以模擬各種情況和錯誤。

偽造物件種類?

但其實偽造物件有各種不同的種類啦XD,他就像一把刀,若我們眼前有個壞人,我們可以砍他,可以刺他,更可以用小李飛刀的方式丟他,但最終目的都是為了打敗眼前的壞人,就跟偽造物件可以用各種方法,但最終目的只是為了消除環境相依性!以下簡單介紹幾個偽造物件,他們的定義有種魔力,看完還是看不懂在幹嘛。

  1. Dummy:偽造物件不參與實際行為,只用來滿足填入參數。

  2. Spy:偽造物件保有原始物件的功能,只有部份函式被仿照。

  3. Mock:偽造物件模擬外部相依介面的互動方式。

  4. Stub:偽造物件參與回傳值的處理以驗證被測試物件。

  5. Fake:偽造物件使用到靜態方法,或直接模擬相依物件的行為。

那來講講我在寫登入功能時真實有用到的偽造物件有哪些?我有用到的是最常被使用到的stub與Mock。以下來簡單介紹這兩種與我在專案怎麼使用他~

測試小撇步:偽造物件Stub

Stub定義 : 偽造物件參與回傳值的處理以驗證被測試物件。

Stub白話說明:stub不會導致測試失敗,測試的對象會是待測試物件,stub只是輔助讓測試更順暢。

使用套件: https://github.com/stretchr/testify的Mock方法

以登入來舉例:模擬service層進入repository層之函式的回傳值,無論輸入甚麼到物件中,必定輸出我預期的結果。

模擬物件會紀錄所有的互動資訊。

測試小撇步:偽造物件Mock

Mock定義 :偽造物件模擬外部相依介面的互動方式。

Mock白話說明:mock會導致測試失敗。測試的對象會是外部依賴元件與待測式物件間的互動,mock會影響測式物件的行為。

使用套件: https://github.com/DATA-DOG/go-sqlmock

以登入來舉例:repository層的模擬Database取代掉真實的 Database 。

測試覆蓋率

當寫完所有測試,為了要讓其他隊友們知道我測試到寫得好不好,可以給出覆蓋率測試報告。但覆蓋率測試其實不是越高越好喔,到底是為啥呢?看下去就知道啦~~

  • 下圖是覆蓋率測試的單元測試的覆蓋率輸出結果,只要寫好測試打出這個指令就會出現了!指令的意思是要把它輸出成profile.out,這時你的檔案跟目錄就會出現profile.out這個檔案了
go test –cover ./… -coverprofile profile.out

測試函式覆蓋率

我們也可以對函式呼叫執行率來當成輸出結果,還記得上一篇有提到單元測試的單元可以理解成對最小單位進行測試,最小單位就是我們的函示啦~指令的意思是你可以直接拿剛剛輸出的檔案做成函式覆蓋率測試輸出。

go tool cover -func=profile.out

測試覆蓋率做成html檔給主管看

為了證明我們的單元測試績效,可以輸出成html給大家看,哪裡沒跑到測試一翻兩瞪眼XD,指令的意思是我們要拿剛剛做好的profile輸出成coverage.html,這時程式碼根目錄就會出現coverage.html。

go tool cover -html=profile.out -o coverage.html

如果單元測試都沒跑到就會出現以下滿江紅。

最理想的模式就是單元測試都有跑到,滿江綠。

程式覆蓋率越高越好?

大家覺得100%程式覆蓋率 vs 90%程式覆蓋率,100%程式覆蓋率一定比較好嗎?

單元測試可以透過計算程式碼被執行的效率來得知目前專案中,有多少程式被測試過,藉此來衡量測試的品質。 但覆蓋率的結果只代表了你程式碼是否有正確被測試執行到 你可以把程式覆蓋率水到100%,報表寫得非常好看,還記得Clean Code的First原則中的T:Timely嗎?他希望寫完程式碼後測試寫的越及時越好,更希望可以先寫測試再寫程式,但也因為這個因素,有沒有測試到核心其實只有開發工程師自己知道,測試覆蓋率高不代表測試寫得很好, 所以我們更應該追求測盡所有的使用情境。包括boundary value、數量龐大的資料集、資安、大數據、例外處理與非預期的函數數量或輸入,根據你的商業邏輯來寫出正中核心的測試。

Timely:不要讓一個測試高度相依其他測試,測試的失敗會影響其他測試也跟著失敗,會很難找出問題癥結點

測試之理論面重點回顧

  1. 釐清目標的測試範圍
  2. 偽造物件的使用
  3. 單元測試的好處
  4. 進行覆蓋率測試

總結

單元測試不像表面那麼簡單,原來也是有好多細節可以琢磨的阿,有聽說過有些公司面試會考單元測試寫出來。但說實話單元測試其實不一定要寫啦,他能確保程式品質,但其實沒有的話產品依然能上線啦。延伸閱讀:程式是人寫出來的,測試也是,之後會介紹怎麼配置drone CI並用AWS進行CD。 (2023 5.27 21:43更新) 算了…好懶喔,好佩服IT邦鐵人賽30天可以完賽的人,要極度克服惰性,佩服佩服。話說原本想要講怎麼用drone+AWS進行CI/CD,但真的被懶惰惡魔奴役了,先去打薩爾達傳說了,有人期待我更新的話左邊資訊欄有我的信箱,寄信給我讓我知道XD,下方是我的go專案登入的單元測試github連結,我都放在那裏了,去找吧! github連結