趁著跑單元測試的時間,來把我腦袋飛亂想撰寫此篇的內容寫出,實現作為後端小菜鳥的Q1 OKR
在前幾篇提及,從做為一個萌新點擊新的品質(quality)三部曲技能樹,除了重構外,第二個同時間學習的大重點為單元測試 Unit Test
延伸閱讀: [我在新創科技業的日子] 迅速與品質
說到單元測試除了品質三部曲外,另外的就是敏捷軟體開發中,極限編程方法(Extreme programming)的測試驅動開發, TDD
這些概念,早在我研究所時期,修過的專案管理中接觸,沒想到進了公司做軟體開發,實際接觸到當時僅存在於書本上的概念時特別感到新奇與感動
當時在做TDD的介紹時,單純認為怎麼可能先在開發前就寫好測試,那要怎麼寫?直到進了公司後,隨著mentor的引導,讓我接觸到單元測試後,才懂這一切的運作
單元測試的功用
依據測試驅動開發的wiki解釋,
測試驅動開發(TDD)是戴兩頂帽子思考的開發方式:先戴上實現功能(Achieve Feature)的帽子,在測試的輔助下,快速實現其功能;
再戴上重構的帽子,在測試的保護下,通過去除冗餘的代碼,提高代碼質量。
測試驅動著整個開發過程:首先,驅動代碼的設計和功能的實現;其後,驅動代碼的再設計和重構。
總結上,單元測試可以做到 實現功能 + 重構
以及確保程式的品質
不太像純寫網頁可以藉由資訊為呈現或排版亂掉來馬上得知有無錯誤,後端程式的開發居多無法透過介面得知是否有錯誤,僅能透過OutPut的值簡單判斷以及掌握DEBUG工具來釐清錯誤根源。
那麼先撰寫單元測試再撰寫功能程式碼的流程,主要是可以通過寫測試來假設
使用情境,釐清這個函式/功能會怎麼使用,預想他應該要獲得什麼答案以及他會實現什麼功能,進而達到設計/假設會需要幾個函式/設計模式/物件來達成此功能。
像是我們要假設達成一個讀取Excel並轉成json的功能,當做完單元測試後,也許我們會假設函式如下
def read_csv(csv_file):
# 讀取excel file
pass
def trans_csv_to_json(data):
# 轉換excel to json
pass
類似這樣的假設函式,然後再慢慢的實作功能以達到符合測試及達成功能需求
單元測試的3A: Arrange, Act, Assert
初次見到 3A
這個名詞是我的mentor在撰寫我的新手訓練文件時所使用的詞,在我還萌新時期(我現在也還是個萌新啊!),我以為這是個三個不同的概念,這讓我當初對單元測試
引發了感覺很深不可測的幻想
3A: Arrange, Act, Assert
意思是一個單元測試會由3A的概念組成
Arrange: 假設測試的場景, 像是準備假資料, 準備變數, 相關函式
Act: 執行測試的目標, 可能是函式/物件...
Assert: 驗正結果是否正確, 撰寫期待的符合條件
在舉一個例子,假設有一個功能是要測試database的資料寫入
Arrange
# 在這個場景中,有可能實際存入的Database在客戶端
# 因此 Arrange 可能是做一個假的FakeDataBase
def setUp(self):
FakeDataBase = open("test.sqlite") # 好吧我承認我暫時寫不出來xD
# 就是準備一個假的sqlite 來假設有db的場景
def tearDown(self):
# 記得假設函式在做完測試後 clear db
Act
# 佈置好準備測試的函式/物件及功能...
from py_file import inset_data
# 準備要輸入的假資料
prepare_data = {'customer', 'Amy'}
# 測試目標函式
inset_data(prepare_data)
Assert
# 假設資料已經輸入進去了
# 也許用pymysql, sqlalchemy 之類套件做讀取
with engine.connect() as connection:
data = pd.read_sql_table(database), connection)
# 可以利用各種assert 方法來驗證期望的答案
self.assertTrue(data = ['customer', 'Amy'])
self.assertEqual(['customer', 'Amy'], data)
自身感想
在剛開始接觸開發後端時,由於是開發網頁
可以直接render前端,萬一有錯誤,大不了就是前端炸掉沒看到東西。無論是除錯或開發我都還能用ipython
或 python console
來做驗證。
但是當我還是超級小萌新時期
,剛開始開發公司的產品,由於沒有前端能檢視錯誤,完全不知道該怎麼實作,不知道前段的函式會送什麼格式與資料,也不知道我要實作的函式要送出什麼資料才不會造成後續的功能錯掉。
雖然跟著前個mentor學使用debug工具追蹤程式碼後,也還是不知道怎麼實作,我也無法在一堆連續性互相依存的函式中尋找進入點,驗證我實作的功能,有段時間十分痛苦
起初接觸單元測試時,由於不清楚不了解,光撰寫單元測試的時間可以佔掉整體開發時間的70%,讓我的實作時間大幅降低,讓我覺得很生氣。有時候甚至先實作功能,才來寫單元測試,往往因為寫單元測試花更多的時間,那時候就覺得很不必要。
現在經歷一段時間了,有天才發現單元測試的神奇,就是可以透過測試來設計這個功能會怎麼被使用,最後會有什麼結果。
當開發一個新的功能,實作會有什麼函式,我都能大致上列出來
缺點
貌似上述所言都著重在於單元測試的好
,接下來來講一下缺點
平衡一下
有好就有壞!
根據wiki的負面評價
1. 開發者可能只完成滿足了測試的代碼,而忽略了對實際需求的實現。
我的想法是有時候我自己真的是因為懶惰加上工作量很大需要快速開發時,我就只會針對 當前的函式
做簡單的測試,絲毫不會考慮前者函式與後者函式會不會有接不上的問題。
2. 會放慢開發實際代碼的速度,特別對於要求開發速度的原型開發造成不利。
雖然可以透過測試來假定使用方法與期望結果,但先是要撰寫單元測試就會花費不少時間,更別說之後要撰寫實際的代碼,有時候撰寫到最後也會發現開發完實際代碼與測試不符,需要將測試做修改
3. 對於GUI,資料庫和Web應用而言。構造單元測試比較困難,如果強行構造單元測試,反而給維護帶來額外的工作量。
第一句不明瞭,但確實有時候"無法"撰寫單元測試,“無法"切割為單元,甚至需要專案內全部的代碼run過一遍才能驗證答案,這就變成整合測試。
4. 使得開發更為關注用例和測試案例,而不是設計本身。
嗯,有的時候當單元測試完成後,撰寫實際程式代碼會為了 “符合” 單元測試,而並非完成原先的功能需求。若測試與程式碼設計本身有矛盾衝突時,寫起來會很困擾
5. 測試驅動開發(先寫測試再實作, TDD)會導致單元測試的覆蓋度不夠,比如可能缺乏邊界測試。在實際的操作中,和非測試驅動開發一樣,當代碼完成以後還是需要補充單元測試,提高測試的覆蓋度。
測試會過就好啦,還邊界不邊界個什麼呢?
qq, 所以說 先寫測試在實作
會花費比較多時間在寫測試,而後續的實作很容易會變成我要通過測試
而努力,偏離原先的設計。
再來若先做實作在測試,會變成花更多的時間在過度設計
實作,測試就亂寫一通 xD
所以上述第五點提及,先撰寫測試,完成實作後,再回來補充單元測試,提高測試的功用!
無論先寫測試還是後寫,只要前輩壓著你寫,你就是得寫