學習筆記 第一部分 測試驅動開發基礎
原創時間: 2016-01-06
更新時間: 2016-01-06
TDD簡介
Test-Driven Development,驅動測試開發,是一種軟體開發的開發方式.
它要求在編寫某個功能程式碼之前先編寫測試程式碼,然後只編寫使測試通過的功能程式碼,通過測試來推動整個開發的進行.
我看的這本書叫,可以通過訪問官網來購買或免費線上閱讀
此書講解TDD的方式是通過Django框架來開發一個Web應用.
我在學習的過程中感覺收穫良多,因此將學習的過程和心得體會記錄下來,希望對想要學習瞭解TDD的朋友也能提供一些幫助.
因為TDD的規則跟傳統的開發方式不同,所以如果要使用TDD,就得轉變以前的傳統觀念.這一點其實挺難的
測試分類
功能測試 Functional Test
以使用者的角度來在外部測試應用,可以寫一個小故事,來模擬使用者的整個操作流程,根據這個流程來寫出測試程式碼,從這個角度上說功能測試可以看作是你開發的應用(或者模組)的一份使用說明書
功能測試可以只在一個模組內,也可以包含多個模組,不過按道理講使用者是不瞭解也不需要了解他們需求的功能是在一個模組內還是包含多個模組的,所以我認為功能測試應該是一個全域性的測試方式
功能測試也叫驗收測試,端對端測試,黑盒測試
功能測試使用的主要的工具是Selenium,一個可以控制瀏覽器行為的工具集
單元測試 Unit Test
以程式設計師的角度從內部來測試應用,測試的內容是某個模組內部的具體實現
單元測試只負責測試某個模組內的功能
TDD開發流程
先借用書中的一張圖來有一個直觀的印象
遍寫一個功能測試
從使用者角度用故事的方式來描述新功能,將故事翻譯成測試程式碼執行功能測試
如果功能測試出錯,就考慮如何編寫應用程式碼來通過測試,
如果功能測試出現的異常是預料中的,其實是好事,至少我知道下一步的工作是什麼
如果功能測試出現了非預期的異常,需要仔細閱讀出錯程式碼,很可能是因為之前的修改導致了更多的錯誤.可以通過多種方式來除錯程式碼:- 新增
print
語句列印執行過程 - 改進錯誤輸出來獲得更多的當前狀態的資訊
- 手動訪問網站
- 新增
sleep
函式來暫停執行過程
- 新增
編寫一個(多個)單元測試
針對上一步想到的編碼方式,編寫對應的單元測試執行單元測試
編寫應用程式碼
針對單元測試提示的錯誤來編寫儘可能少的應用程式碼,僅僅是為了通過這次測試
迴圈第3,4,5步,直到通過全部單元測試並且覺得能通過第1步編寫的功能測試執行第1步所編寫的功能測試
如果能通過,則可以向前一小步了
如果不能通過,可能需要編寫更多的單元測試和應用程式碼,繼續迴圈第3,4,5步重構程式碼
不修改程式碼的功能而又對程式碼進行修改的過程就叫重構
當通過單元測試或功能測試時考慮是否需要重構程式碼,注意在重構的過程中也要遵循TDD的規則,每重構一小步就要執行一次測試,一直到重構完成.當測試全部通過時,又回到第1步(或第3步)
重構程式碼也包括修改單元測試的程式碼,但是注意修改測試程式碼和修改應用程式碼不能同時進行
以上的流程看起來很繁瑣,也很無趣,還很浪費時間,但是我覺得這樣做有他的道理和好處
保持編碼的連續性,當功能測試寫好以後,整體目標就定下來了,以後的工作就是根據測試提示的錯誤來一步一步的接近目標,而不會受到其他的影響,比如在實現功能的過程中又有其他的想法,轉而開發其他的模組,也不會過早地進行重構
在整個開發過程中只需要考慮怎麼通過測試就好了,不需要考慮太多的其他問題,這樣的開發過程也會相對輕鬆
整個開發過程都受到單元測試的保護,保證不會出現錯誤,也不會破壞以前已經實現了的功能和模組
在開發簡單功能時會感覺很無趣,但是當代碼結構已經很複雜時,可以避免被複雜的程式碼攪暈
Kent Beck有一個很好的比喻,開發的過程就像是從井裡打水,當井不深水桶也不夠滿時,這個過程很容易,但如果要從一個很深的井裡面打一桶很滿的水,這個過程就很困難了.而TDD就像在井口安裝的一個棘輪,它不一定能讓你打水更輕鬆,但能讓你在打水的過程中能稍事休息,還能保證你的水桶不會突然滑下去.就像打RPG遊戲時的SL大法一樣(這個比喻是我想出來的^_^)
TDD是一種紀律,如果不一開始就對你要求嚴格,以後你對自己的要求會越來越鬆的
TDD的規則
在做任何事情之前:寫測試
這是TDD最基本的規律,在做任何事情之前(新增功能,修改功能,重構程式碼),都要先把測試程式碼寫好.一開始會感覺這種方式反人類又浪費時間,其實在熟練運用以後會體會到這樣開發的好處
每次只走一小步
當功能測試通過後需要新增新的功能時,不要想太遠,只向前走一小步,新增一點點新的功能就好.
當單元測試失敗時,只改動最少的程式碼量(一次一行)以通過當前測試就可以了
我每次寫程式碼都會忍不住把想到的都寫出來,想改的都改完,這樣的結果就是經常把程式碼搞得一團糟,連以前已經實現的功能都不能正常運行了.反而會花更多的時間來除錯混亂的程式碼.
關於每次只改動最少的程式碼量這個問題我是這樣來理解的,每次改的最小程式碼量指的是你最容易控制的程式碼量,不一定非得只能是一行
比如說views裡面缺少一個方法home_page來處理一個url,對於初學者,修改迭代的步驟是這樣的:
# 第一步,定義一個home_page空物件
home_page = None
# 第二步,將home_page空物件改為一個空方法
def home_page():
pass
# 第三步,對home_page方法新增必須的引數
def home_page(request):
pass
# 第四步,修改home_page方法的返回值,使之返回HttpResponse物件
def home_page(request):
HttpResponse()
# 第五步,返回正確的響應
def home_page(request):
HttpResponse('something')
但是對於一個已經熟練掌握Django的程式設計師來說,我認為直接寫成如下形式是沒問題的
def home_page(request):
pass
使用版本控制
基本的規則就是當需要改功能加功能或者重構之前,先commit一次
因為我依賴IDE的history功能,所以commit得沒那麼頻繁
一定要壓抑住自己的創作熱情
在做任何改動之前先寫測試,如果你因為自以為程式碼邏輯簡單就忍不住越寫越多,當你意識到程式碼足夠複雜時,你已經是那隻被煮熟的青蛙了.
每個測試方法只測試一件事
每個單元測試的方法要儘可能的小,每個測試方法只測試一件事情
這樣的方式會讓你在測試失敗時輕鬆定位失敗的原因
並且如果當測試方法中前面的測試出錯時,你也不知道後面的測試是否通過
不要測試常量
單元測試的目的是測試邏輯,執行流控制,配置等,而不是測試常量.如果在應用程式碼裡寫了
wibble = 3
這時如果寫這樣的測試程式碼就沒有多大意義了
assert wibble == 3
如果需要測試HTML字串,將HTML字串寫在模板(templates)裡面
重構程式碼就只重構程式碼
在重構完成之前一定不能增加或修改功能,重構就只是重構,這也是我之前在開發時經常會犯的錯誤,經常在重構時又搞到其他地方去了.同樣的道理,在重構沒有完成之前也不要修改測試程式碼
重構程式碼的時機
- 三則重構原則(Three Strikes and Refactor)
當第一次複製已有的程式碼時(這樣相同程式碼就出現了兩次),忍了,不要急著來重構它
當再一次複製相同的程式碼時,這時相同的程式碼已經重複三次了,重構吧,注意重構之前先commit一次程式碼
遵循這個原則可以避免過早的重構程式碼
儘量不要中斷
如果在寫測試程式碼或編碼時突然有什麼新的想法,儘量不要中斷當前的工作
拿一張便條或者在TODO文件中記錄下來,等這輪unit-test/code週期完成後,再來處理
或者等功能測試通過後,新增為新的功能
YAGNI
YAGNI(You aint gonna need it!)意為你不需要它.每個開發人員都樂於創造,有時很難壓抑馬上實現一些頭腦中突然冒出來的想法的衝動,但這些想法很可能是不需要的.
因此在剛開始設計應用時不要設計得太複雜,功能也不要太多,只要設計最基本的功能就好了
更復雜的設計和功能在開發中根據現實的需求逐漸加入.
這一點我覺得很有道理,以前在早期設計時總想著如何把所有可能的功能和需求都考慮到,浪費了很多的時間和精力,但真正在開發中會發現需求在不斷地變化,以前設計的很複雜的模型可能根本就沒用.
TDD開發中的一些技巧和需要注意的問題
在編寫針對某個views方法的單元測試時,首先需要測試兩點:
系統是否能將請求送達到對應的views方法
views方法是否能返回我們需要的響應
每次處理完POST請求以後都要重定向
在功能測試程式碼中用兩個井號”##”開頭的註釋來作為註釋的註釋(元註釋)以區分普通的註釋
在功能測試中普通的註釋主要描述使用者在幹什麼
而元註釋則解釋測試程式碼是如何工作和這麼工作的原因如果在功能測試時出現了什麼奇怪的問題,先升級Selenium
pip3 install --upgrade selenium
參考程式碼
此文包含書中的第1-6章,每一章的程式碼對應程式碼庫中的一個分支,其中