書籍連接: book.douban.com/subject/259…數據庫
這本書教你爲何要關注可測試性, 如何編寫可測試的代碼, 以及如何推進測試落地.bash
書中的代碼是.NET的, 不太習慣, 後文中我改爲用Golang進行舉例.網絡
第一部分是入門章節, 告訴咱們什麼是單元測試, 爲何要用單元測試, 如何寫好單元測試.架構
什麼是單元測試? 單元測試是一段自動化的代碼, 這段代碼調用被測試的工做單元, 以後對這個單元的單個最終結果的某些假設進行檢驗. 這裏須要注意一個概念: 單元. 從調用系統的一個公共方法到產生一個測試可見的最終結果, 其間這個系統發生的行爲總稱爲一個工做單元.框架
一個相關的概念是集成測試. 單元測試和集成測試的最主要區別在於: 是否使用了真實依賴物, 如數據庫, 網絡鏈接等等.函數
什麼是好的單元測試? 優秀單元測試應該有以下特性:單元測試
在開發過程當中應該什麼時候編寫單元測試? 這個見仁見智, 能夠採用TDD先寫測試後寫功能, 也能夠寫完功能再補測試.測試
單元測試的目的在於使得代碼可維護. 若是你的單元測試沒有促進這一目標, 就要反思是否是真的寫好了單元測試.ui
核心技術章節主要介紹瞭如何使系統與外部依賴項隔離, 從而進行去依賴的單元測試.編碼
一般咱們開發的系統都會有各類外部依賴項. 外部依賴項是系統中的一個對象, 被測試代碼與這個對象發生交互, 但你不能控制這個對象. 常見外部依賴項包括文件系統, 線程, 內存以及時間等. 注意: 一旦你的系統中引入的真實的外部依賴項, 那麼你進行的就是集成測試, 而非單元測試.
顯然, 若是無法控制外部依賴項的行爲, 就沒法保證單元測試運行結果的穩定性. 那麼如何使外部依賴項可控? 答案是使用僞對象 (fake) 替代真實的外部依賴對象.
那麼接下來的問題是, 如何用僞對象替代外部依賴? 只須要找到被測試單元使用的外部接口, 而後將接口的底層實現替換成你能控制的代碼. 若是這個接口與被測試單元直接相連, 就添加一個間接層, 隱藏這個接口. 來看下面這個例子:
// 判斷文件是否存在
func IsFileExist(fileName string) bool {
_, err := os.Stat(fileName)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
複製代碼
這段代碼引入了文件系統依賴, 須要用一個僞對象替代真實文件系統. 然而, 文件系統有關的代碼已經寫死在函數裏了, 這就須要引入一箇中間層, 抽象出文件系統的操做. 這裏咱們聲明一個IFileManager接口:
type IFileManager interface {
IsFileExist(string) bool
}
複製代碼
提供一個真實的文件系統實現和一個僞對象實現:
type PureFileManager struct{}
func (t *PureFileManager) IsFileExist(fileName string) bool {
_, err := os.Stat(fileName)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
return false
}
type FakeFileManager struct{}
func (t *FakeFileManager) IsFileExist(fileName string) bool {
return true // 行爲徹底由咱們控制, 這裏無腦返回true
}
複製代碼
改寫以前的IsFileExist()函數:
func IsFileExist(fileName string) bool {
var mgr IFileNameManager = new(PureFileNameManager)
return mgr.IsFileExist(fileName)
}
複製代碼
這樣雖然添加了一箇中間層, 可是仍是與外部依賴綁定了. 解決辦法是: 使用依賴注入, 在被測試單元中注入一個僞實現. 依賴注入有兩種方法: 構造函數注入和屬性注入. 如何選擇? 若是依賴項是必須的, 就用構造函數注入, 不然儘可能使用屬性注入. 下面爲咱們最終改造後的代碼, 經過這樣的改造, IsFileExist()函數就與文件系統的強依賴解耦了, 能夠進行單元測試了.
var mgr IFileNameManager
func SetPureFileNameManager() {
mgr = new(PureFileNameManager)
}
func SetFakeFileNameManager() {
mgr = new(FakeFileManager)
}
func IsFileExist(fileName string) bool {
return mgr.IsFileExist(fileName)
}
複製代碼
一個工做單元可能有三種最終結果: 返回值, 改變系統狀態, 調用第三方對象. 對單元測試來說, 前兩種結果與第三種有一個顯著區別: 返回值和改變系統狀態都是在當前系統中可觀測到的, 能夠直接對當前系統進行斷言以驗證測試結果正確性. 而調用第三方對象時, 當前系統的狀態有可能未發生任何改變, 驗證測試結果正確性須要對第三方對象進行斷言. 這就引出了另外兩個概念: 存根 (stub)和模擬(mock).
fake對象既能夠做stub, 也能夠做mock, 區別在於: 測試結果是否依賴對fake對象的斷言 (也就是書中說的: stub不會致使測試失敗, 而mock能夠). 若是是, 那fake對象就是mock, 不然就是stub. 換言之, mock關注工做單元對外部依賴影響, stub關注外部依賴返回給工做單元的結果.
來看下面這段代碼. 這一段實現了這樣一個功能: 若是是星期六, 天氣下雨, 就訂一份外賣:
var kfc KFC // KFC餐廳
var address string // 個人收貨地址
type KFC struct {}
func (t *KFC) OrderLunch(address string) {
fmt.Printf("Lunch is send out to %s, please wait!\n", address)
}
func DailyTask(date, weather string) {
addr := GetMyAddress()
if isSatuday(date) && isRainy(weather) {
kfc.OrderLunch(addr) // 給KFC發送一個訂餐事件
}
}
複製代碼
咱們如何肯定這個DailyTask()是否真的會在星期六的雨天發送這樣一個訂餐事件? OrderLunch()方法並無返回值, 也沒有改變被測系統的狀態, 須要檢查KFC內部的狀態. 所以, 咱們引入一個MockKFC對象, 調用該對象的OrderLunch()能夠記錄下傳入的address參數.
var kfc IKFC // 把KFC變成一個接口, 便於注入mock對象
type IKFC interface {
OrderLunch(address) // 送餐
}
type MockKFC struct{
TestAddr string
}
func (t *MockKFC) OrderLunch(address string) {
t.TestAddr = address
fmt.Printf("Lunch is send out to %s, please wait!\n", address)
}
複製代碼
這樣改造以後, 測試代碼就很好寫了:
func TestDailyTask(t *testing.T) {
kfc = new(MockKFC)
address = "zju"
DailyTask("Saturday", "rainy")
if kfc.TestAddr != address {
t.Error("address not equal")
}
}
複製代碼
通常來講, 咱們不會手動管理mock對象, 而是採用mock框架幫助咱們完成這些事情. 書中第5,6章介紹了mock框架方方面面, 包括工做原理, 分類, 模式等等. 沒太仔細看.
主要介紹了測試代碼的組織方式 (第7章) 以及編寫測試的最佳實踐 (第8章).
首先須要明白的是, 對於一個應用程序而言, 單元測試和產品源代碼同等重要. 測試代碼一樣須要維護.
優秀的測試應該同時具備以下三個屬性: 可靠性, 可維護性, 可讀性.
關於如何編寫可靠的測試, 做者給出了幾點建議:
單元測試命名很是重要. 一個測試名包含三個部分: 被測試方法名, 測試場景, 預期行爲. 例如: Sum_ByDefault_ReturnsZero()
注意單元測試中的變量命名規範, 不要出現magic number, 應該在變量名中反映出變量的含義.
給出有意義的斷言信息, 可以清晰地反映測試結果. 同時在代碼上要將斷言和操做分離, 不要把斷言和操做寫到一行裏面.
不要濫用setup和teardown. 初始化模擬對象, 設置預期值這些操做應該放到測試方法中, 而不該該放到setup中. teardown通常用於集成測試, 在單元測試中, 只會在重置一個靜態變量或單例的狀態時纔會使用.
這一章跳出了單元測試的技術細節, 轉而從管理的角度探討了如何推進單元測試在團隊中落地.
如何在組織中引入單元測試? 做者給出的建議是: 小團隊, 低風險項目, 領導者願意接受變革. 須要注意, 必定要在確保你瞭解單元測試的基礎上, 再推進單元測試, 萬不可倉促實施單元測試. 此外, 還須要必定的政策上的支持.
做者指出, 要開始單元測試, 至少須要30%的工做時間. 然而, 引入單元測試並不必定會致使總體流程時間增長. 進行單元測試更容易在開發期修復bug, 從而減小集成測試的時間, 項目的交付期有可能提早.
單元測試是否會搶了QA飯碗? 不會的, 單元測試的存在會使QA更專一於尋找實際應用中的邏輯缺陷, 讓QA專一於更大的問題. 有些公司QA工程師也寫代碼, 開發者和QA工程師均可以編寫單元測試.
做者有一句話寫得很是好: 你須要使用單元測試, 確保人們知道代碼的功能是否受到破壞.
編碼是代碼生命週期的第一步. 在生命週期的大部分階段, 代碼都處於維護模式. "大部分的缺陷並非來自代碼自身, 而是由人們之間的誤解, 不斷變化的需求以及缺乏應用領域知識形成的."
對一個遺留項目, 如何從0到1開始單元測試? 首先你須要列出項目組件的測試優先級. 能夠經過邏輯複雜度, 依賴數, 重要程度判斷其優先級. 通常來講, 邏輯驅動的容易測試, 依賴驅動的難以測試 (須要mock).
在肯定了優先級以後, 須要選擇測試策略, 先易後難, 先難後易, 各有優劣.
在重構代碼前, 先進行集成測試, 確保重構時不會破壞原有功能.
什麼是可測試的設計? 就是代碼架構便於進行測試. 而測試的關鍵在於"接縫". 對靜態語言來講, 須要主動採用容許替換的設計 (即提供接口), 代碼才能得到可測試性. 而對於動態語言, 可測試性設計就顯得不那麼有意義.
可測試的設計與SOLID原則相關, 通常來講知足SOLID原則的設計都是可測試的設計, 而反之不成立, 所以: 可設計性並非優秀設計的目標, 而是優秀設計的副產品.
可測試設計會增長工做量, 編寫更多的代碼. 能夠首先使用簡單設計, 在須要時再進行重構.
書中提到了一些其餘的技術書籍, 我的以爲有些書籍還不錯, 列舉以下: