Q: 為什么通過單元測試發(fā)現(xiàn)的 Bug 很少 ?
A: 單元測試不是用來發(fā)現(xiàn) Bug 的, 而是用來預防 Bug 的. 如果采用 TDD, 測試用例完成之時, 產品代碼尚未編寫, Bug更無從談起.
Q: 那是否寫單元測試就能提高代碼質量了 ?
A: 關于這一點, 似乎有人不這么看, <<TDD Opinion: Quality Is a Function of Thought and Reflection, Not Bug Prevention>>. 不錯, 代碼質量并不必然關聯(lián)到單元測試, 諸如凈室軟件開發(fā)之類的方法依然可以在沒有單元測試的情況下得到高質量的代碼, 但這是另外一個問題. 或許主觀上, TDD的本質更接近于促使你把質量內建在思維中, 但客觀上, 在其它條件都相同的情況下, 單元測試依然能夠起到預防 Bug 的作用.
Q: 單元測試怎么能反映/代替需求 ?
A: 單元測試未必能直接反映宏觀上的需求, 但
-
功能測試和集成測試能夠反映宏觀需求.
-
單元測試能夠反映系統(tǒng)的其它部分對當前單元的需求.
而從文本的角度, 測試用例的名字就是需求的描述. 換句話說, 你從傳統(tǒng)的需求文檔中把描述摳出來, 放到測試代碼中作為測試用例的名字, 你便擁有了可執(zhí)行的需求文檔
一個 RSpec 寫的功能測試用例 (不要懷疑, 它確實是可以運行的):
it "should show welcome message after login" do
login_as_chelsea
get :index
response.should have_text(/歡迎 chelsea/)
end
it "should not show welcome message after logout" do
logout
get :index
response.should_not have_text(/歡迎/)
end
單元測試的例子:
public void testShouldBeFreeFrom2amTo5am() throws Exception { //直接業(yè)務需求
...
}
public void testShouldThrowExceptionIfCannotFindConfigFile() throws Exception { //來自系統(tǒng)其它部分的需求
...
}
測試用例并不排斥業(yè)務層面的需求文檔, 一個高層的, 突出業(yè)務價值的需求/愿景描述對于快速理解系統(tǒng)是非常有幫助的, 但/只是測試用例以另一種方式描述了真實的系統(tǒng), 它具有兩個突出的優(yōu)點:
-
它不會說謊, 即永遠與系統(tǒng)真實的行為同步
-
它是可執(zhí)行的, 它可以不知疲倦的, 成本極低的, 時時刻刻, 反反復復的追問你的系統(tǒng)是否符合需求
Q: 需求變了怎么辦? 豈不是有大量測試用例需要修改?
A: 難道不是應該的嗎? 難道以前的需求文檔在需求發(fā)生變化時不需要修改? 哦, 或許它們不需要, 因為沒人會關心, 對代碼也沒什么影響, 需求文檔在度過最初的幾周后便被扔在配置庫里再也沒人管它了.
是的, 這關系到你的測試策略. 然而通常的測試策略對單元測試的要求都是盡可能周全.
于是這就是一個測試設計的問題. 是的,測試代碼也需要設計, 也需要重構, 也需要
Domain Specific. A: 這是集成測試, 不是單元測試. 你一定把系統(tǒng)所有的組件都編譯鏈接起來了.
那么如果你的測試失敗了, 是哪一部分的問題呢? 通常單元測試需要滿足一個條件: 不依賴任何其它單元, 即隔離性. 實現(xiàn)手段就是在測試環(huán)境中能夠輕易的假冒依賴,
并設定依賴按照我們的意愿進行工作. 一個例子就是你的代碼依賴 malloc 獲取內存, 而你想測試內存不足的情況.
那么我們應在能夠/需要在單元測試中使用使用一個假冒的 malloc 來代替真正的 malloc, 并且我們能控制假冒的 malloc 返回 NULL
以模擬內存不足的情況. 關于如何做到這一點, 可參考一些成熟的"假冒"框架, 如
mockcpp 等. A: 其它單元有其它單元自己的單元測試, 各自關注自己. 集成測試像以前一樣, 該怎么測還怎么測,
并不是有了單元測試就不要其它測試了. A: 單元測試反映的是局部的設計, 局限于本單元以及與之交互的其它單元.
前面說的單元測試能夠反映系統(tǒng)的其它部分對當前單元的需求, 所謂設計就是單元之間的職責劃分, 交互和依賴關系 當你試圖測試一個單元時, 卻發(fā)現(xiàn)需要創(chuàng)建大量的其它對象, 而且按照你腦海中的實現(xiàn),
有些對象是在單元內部創(chuàng)建的, 根本無法在測試環(huán)境中假冒它們. 這時候, 你即使只是為了減少測試的難度, 也會逼迫自己思考: 這個單元是否做了太多的事, 承擔了額外的職責, 違反了單一職責原則? 是否應該把依賴讓外界設置進來, 而不是自己在內部創(chuàng)建, 這樣測試時就能把依賴設置為假冒的實現(xiàn)? 是的, 單元測試警示你思考一下自己的設計 A: 這實際上是另外一種角度. 源代碼就是設計的論斷基于兩個假設 設計階段中工程師的工作產物, 也就是他的設計,
是應該能夠在實施階段被不同的實施者嚴格并且?guī)缀跻荒R粯拥膶崿F(xiàn) 軟件開發(fā)人員也是工程師, 即軟件工程師 如果我們認同這兩個假設, 那么軟件工程師的什么產物能夠被嚴格并且重復實現(xiàn)的呢?
是你的Word形式的"設計"文檔嗎? 是CAD工具畫出的UML圖嗎? 都不是, 因為它們都不精確, 有無數(shù)種實現(xiàn)方式, 根本談不到嚴格,
不同的開發(fā)人員會有完全不同的實現(xiàn). 事實上, 只有源代碼,才能滿足這個約束. 這樣軟件的設計階段, 就是直到軟件工程師完成源代碼的那一刻, 而軟件的實施階段,
其實就只剩編譯和部署了. 跑題了. A: 名字和斷言描述需求, 環(huán)境設置描述設計 ... A: 黑白都是相對于你觀察的層次. 相對于其它從外部觀察"系統(tǒng)"行為, 不涉及源代碼的測試來說,
單元測試深入到內部觀察盒子的行為, 所以是白盒. 而具體到每個單元測試用例, 依然在盡可能的從外部觀察"單元"的行為, 所以又是黑盒. A: 說來話長, 但可以先說結論: 基于狀態(tài)的測試 over 基于交互/行為的測試,
雖然右邊的也有巨大的價值, 但我們認為左邊的更穩(wěn)定和更富有對系統(tǒng)的洞察力 基于狀態(tài)的測試描述的是需求, 基于交互行為的測試描述的是實現(xiàn). 相對于需求來說, 實現(xiàn)更易發(fā)生變化,
尤其在另外一種實踐"重構"的沖擊下, 描述實現(xiàn)的測試將被修改的面目全非, 帶來相當?shù)姆倒ず途S護成本 一種例外, 就是交互本身就是需求, 這時 mock 是合適的選擇. 一個杜撰的例子參見<<TDD: Tricky Driven Design 3, 方法>>中最后銀行API的例子 而現(xiàn)實生活中, 存在一些情況, 雖然使用 mock 可能帶來后期的維護成本,
但它帶來的好處也是不可代替的. 比如對先期整體測試代碼的編碼量的降低. 這在 C/C++ 項目中尤其明顯: 受限于C/C++的編譯模型, 使用常用的預處理期接入點和編譯鏈接接入點技術來接入
stub 實現(xiàn)時, 要小心維護頭文件的防衛(wèi)宏, 頭文件的名稱, 不同環(huán)境下構建腳本的include路徑設置, 庫路徑設置等.
手工寫stub的方式變的及其繁瑣和容易出錯. 這時候, 一個易用的 mock 框架如
mockcpp 等將節(jié)省大量的編碼和先期維護工作 而幾乎所有的mock框架, 都支持將 mock 對象退化為 stub, 如
mockcpp 中 mock 對象的
defaults() 設置, 或者 JMock 2 中的 Allowing . 事實上, 這是我推薦的 mock 使用方式:
通常情況下讓它退化為簡單的stub, 必要時才使用它強大的期待設置和驗證能力. 通常單元測試有兩個公認的約束需要滿足: 快 隔離依賴. 重申一遍結論就是:
在滿足單元測試的快和隔離依賴的前提下, 優(yōu)先選擇基于狀態(tài)的黑盒測試(可使用手寫stub或mock退化的stub) 除非交互和行為本身就是需求(可使用mock對象的全部特性) A: 把它變成 public 的. 我是認真的. 如果發(fā)現(xiàn) private
函數(shù)無法簡單的通過某個public函數(shù)的測試來覆蓋而需要專門的測試, 意味著你的單元可能承擔了太多的職責, 應該拆分到一個單獨的單元中, 并開放為 public
函數(shù). 如果使用 C++, 在測試環(huán)境中 #define private public. 如果使用 g++, 在測試環(huán)境中加入 -fno-access-control. A: 沒什么好辦法. 這些語言特性和測試的目的是相同的, 都是為提高代碼質量, 減少出錯的可能,
雖殊途同歸, 但卻互相限制, 效果也不一樣. 我認為工業(yè)界是時候嚴肅認真的考慮測試環(huán)境了, 最好在語言中內建對測試的支持,
一些為產品環(huán)境設計的語言特性, 應該在測試環(huán)境中關閉, 而在產品環(huán)境中生效. 其實之前很多編譯器都支持 Release 和 Debug 兩種環(huán)境,
也是從代碼質量的方面考慮的. 現(xiàn)在毫無疑問證實單元測試比 Debug 更有效, 是時候與時俱進增加對 Test 的支持而逐漸罷黜對 Debug 的支持. 在語言本身增加對測試的支持之前, 我們不得不想辦法在測試環(huán)境中繞過語言特性的限制, 尤其對遺留系統(tǒng),
代碼已經存在的情況. 比如對于 C++ 中的 static 函數(shù), 可以將整個被測單元 #include, 或者 #define static 為空.
宏代表了一層間接, 在測試環(huán)境中, 這層間接是至關重要的. 其它方法可參考 <<Working Effectively with Legacy Code>>,
<<假冒的藝術>>中的介紹. A: 有. 如果你發(fā)現(xiàn)不得不 Debug, 就是測試粒度太粗, 步子邁的太大, 產品代碼過長等導致的,
甚至可能你卷入了過多的單元而破壞了測試的隔離性. Debug還是代碼邏輯不清, 行為難以斷言的表現(xiàn). 用測試幫你定位錯誤. A: 特征測試: 保持代碼行為的測試, 獲取當前運行結果, 來填充測試, 以獲取系統(tǒng)目前行為.
其實測試可以分為兩類: 試圖說明想要實現(xiàn)的目標, 或者試圖保持代碼中既有的行為; 在特性實現(xiàn)后, 前者會轉化為后者. 詳細信息請參見<<Working
Effectively with Legacy Code>> A: 還是<<Working
Effectively with Legacy Code>>, 或者<<在大型遺留系統(tǒng)基礎上運作重構項目>> A: 基本一樣, 并且在過程式語言中應用 TDD, 可能會導出面向對象風格的設計.
比如如果直接調用某個函數(shù), 那么不得不通過編譯時替換或鏈接時替換來接入假的實現(xiàn). 這樣其實比較麻煩, 因此可能會促使你選用函數(shù)指針
,以便方便的在測試環(huán)境中進行替換. 隨著時間的推移, 你會發(fā)現(xiàn)一組組概念相關的函數(shù)指針出現(xiàn)了, 那么把它們和它們操作的數(shù)據(jù)綁定在一起, 定義一個 struct,
就形成了一種對象風格. 當然這反而可能會令你的代碼更復雜, 這需要在實踐中取舍. 也有可能在過程式語言中你覺得 TDD 對設計的促進不大, 而且測試用例也比較枯燥, 就是測個分支,
返回值什么的. 是的, 邏輯就隱藏在分支和返回值中, 如果習慣了過程式思維并不打算改變, TDD 對設計的影響則更多的體現(xiàn)在依賴管理上,
如頭文件和編譯單元的職責劃分. 如果把不同職責的函數(shù)混在一個編譯單元里面, 則很難實施鏈接替換等手段, 除非你選擇一個類似
mockcpp 的框架, 不需要鏈接替換. A: 是, 是一開始就要進入項目組, 可不是因為 TDD. 是,
測試人員是一開始沒什么可測的, 可不代表就沒活干. TDD是一種開發(fā)方法, 是開發(fā)人員參與的活動. 其效果是以可執(zhí)行的形式文檔化你的需求,
迫使你分清職責隔離依賴以驅動你的設計, 編織安全網以扼殺Bug在搖籃狀態(tài)防止逃逸. 可傳統(tǒng)測試人員的活動是試圖找到已經逃逸的Bug. 這兩種活動都是必要的,
而且毫不沖突, 互為補充. 那么測試人員在新的特性還沒開發(fā)完成之前做什么呢? 除了提前寫測試用例, 無論是自動化的還是非自動化的,
而需要測試人員參加的一項重要活動, 就是參與特性驗收條件的制定. 之前經常發(fā)生開發(fā)人員按照自己的理解去編碼, 測試人員按照自己的理解去測試, 直到開發(fā)完成,
測試過程中才發(fā)現(xiàn)理解的不一致, 開始產生爭執(zhí)并阻塞等待業(yè)務分析人員(如果幸運的話)或者行政主管(如果開發(fā)過程混亂的話)的仲裁.
解決辦法就是就在開始開發(fā)新特性前的一剎那, 由業(yè)務分析人員, 測試人員, 開發(fā)人員進行一次討論, 就驗收條件達成一致并形成記錄,
然后測試人員和開發(fā)人員分頭去寫測試和實現(xiàn). A: 跟以前一樣, 該有那么個集成測試階段還得有那么個集成測試階段, 取決于產品當時的質量狀態(tài).
并不是說有了迭代級別, 單個特性級別的測試就不需要發(fā)布級別的集成測試了, 兩者沒有任何矛盾. A: 盡早發(fā)現(xiàn)問題, 降低修復錯誤的成本. 有幾種手段,
一是前面提到與業(yè)務人員和開發(fā)者一起討論驗收條件, 這樣就能防止理解偏差而導致的返工. 二是開發(fā)完成立即測試, 發(fā)現(xiàn)問題立即反饋,
這樣開發(fā)人員對代碼依然印象深刻,能快速定位和修復錯誤. 這樣流入最后集成測試階段的Bug就會少, 會縮短最后的集成測試時間, 保證產品更平穩(wěn)的發(fā)布. A: 幾個手段. 測試盡量自動化, 以便能夠持續(xù)集成. 再就是做好依賴管理, 每當一個新特性完成,
就應該能夠發(fā)現(xiàn)它影響的其它特性, 看看是否應該補充一些集成測試. A: 下個迭代測唄, 并且在計算開發(fā)速度時, 只應該計算本迭代通過測試人員驗收的特性,
那些僅僅是開發(fā)人員完成, 沒有經過測試人員充分測試的特性不計在內. 這種情況是不可避免的. 但我們能通過一些手段讓測試與開發(fā)更加同步, 盡量縮短滯后性,
包括讓測試人員與開發(fā)人員更緊密合作, 盡量讓測試用例自動化等. A: 如果這不是您的感覺, 而是事實, 并且前面測試人員必須要做的工作也都做了, 還是不飽滿,
那么恭喜你, 可以省下一些測試人員, 去做別的事了. 但不推薦的是, 不要讓測試人員同時為兩個團隊工作. 這會大大增加溝通的成本. 你會經常發(fā)現(xiàn),
當你的開發(fā)者想找測試人員協(xié)助時, 卻找不到人了, 于是你的團隊便被堵塞在那里. 而測試人員本身的Context切換也是痛苦的. A: 驗收, 當然是由客戶來驗收, 這在理論上是毫無疑問的, 而且肯定在各行各業(yè)發(fā)生著.
只是具體到測試用例的編寫和執(zhí)行, 無論是自動化的還是非自動化的, 都需要掌握一定的技術, 需要周密的思考, 需要專門的時間, 客戶可能無法同時滿足這幾個條件,
我們要盡力爭取, 爭取不到, 便只好通過更充分的交流來彌補越俎代庖的失真. 這時業(yè)務分析人員和測試人員要通力合作, 完成驗收測試的編寫. A: 是增加了 3 倍的代碼量而不是工作量. 它節(jié)省了你幾十人做幾個月龐大的預先設計的工作量,
節(jié)省了你詳細設計每個模塊并為之編寫幾百頁詳設文檔的時間, 節(jié)省了無數(shù)不眠之夜通宵Debug的時間, 它節(jié)省了集成階段修復難以計數(shù)的Bug的工作量,
甚至它縮減了你產品代碼的數(shù)量, 大量的重復代碼被消除了, 大量過度設計的復雜代碼被廢除了, 你的代碼更易理解了, 添加新特性更容易了, 發(fā)現(xiàn)的Bug更易定位了,
以致于大大減少了長達數(shù)年的生命周期內維護的工作量. 有點夸張了? 可這就是 TDD
和敏捷開發(fā)帶給我們的好處(如果你已經實踐了)和vision(如果你還在觀望) A: 沒什么事是非做不可的. 取決于你要什么. TDD 只是以可驗證的方式迫使你將質量內建在思維中,
長期的測試先行將歷練你思維的質量. 而事后的單元測試只是惶恐的跟隨者.Q: 我的單元測試編譯鏈接速度很慢, 而且有些條件很難測, 比如內存不足, 或者環(huán)境很難搭建,
比如需要網絡或數(shù)據(jù)庫, 怎么解決?
Q: 我原來的測試都是用真實的代碼來跑, 一個測試能覆蓋多個單元. 你現(xiàn)在都把依賴替換掉了,
那被替換掉的模塊有問題怎么辦? 怎么保證集成真實的代碼后還能正確工作?
Q: 單元測試就是設計? 單元測試怎么能反映/代替設計 ?
Q: 單元測試是設計, 還有人說源代碼是設計, 到底是測試是設計還是源代碼是設計?
Q: 單元測試是需求"文檔", 單元測試又是設計"文檔", 它怎么能既是需求又是設計呢?
Q: 既然單元測試描述的是需求, 它就應該是黑盒測試了? 可單元測試不一直都被認為是白盒測試嗎?
Q: 但是你們常用的 Mock 技術, 明顯把單元測試推向白盒的境地.
Q: 怎么測 private 函數(shù)?
Q: 類似 private, 一些意圖實現(xiàn)良好設計的語言特性, 如 static, sealed,
final, 非虛函數(shù)等, 卻總是給代碼的易測試性帶來麻煩, 該如何取舍?
Q: 剛才提到了要支持"測試"而不是"Debug", 測試和Debug難道有什么矛盾嗎?
Q: 我知道為遺留系統(tǒng)增加新特性是要先寫測試保證系統(tǒng)原來的行為, 可遺留代碼很龐大,
我甚至都不知道系統(tǒng)目前的行為, 怎么辦?
Q: 有成熟的關于在遺留系統(tǒng)上實踐 TDD 或者單元測試的實踐嗎?
Q: 前面經常說到 C++ 或其它面向對象語言, 卻沒有提到 C, 那么過程式語言中如何應用 TDD ? 有什么不一樣?
Q: 如果使用 TDD, 那么測試人員怎么安排? 是不是一開始就要進入項目組?
可那時還沒有產品代碼,測什么?
Q: 之前會有一個階段, 就是一組相關的特性開發(fā)完成后, 測試人員接手測試, 幾輪Bug修復過去后,
產品基本穩(wěn)定就可以發(fā)布了. 現(xiàn)在測試人員提前介入到每個迭代中, 針對單個特性進行測試, 那如何保證產品集成起來的質量?
Q: 那么測試人員提前進入迭代有什么好處?
Q: 有時候后續(xù)的特性會影響前面的特性, 那么迭代過程中測試人員只測單個特性,
怎么保證以前的特性依然工作?
Q: 有時候開發(fā)人員完成一個特性時已接近迭代結束, 測試人員沒有時間進行充分測試, 怎么辦?
Q: 我還是覺得在開發(fā)迭代過程中, 測試人員的工作量不飽滿.
Q: 你們說驗收測試應該由客戶來編寫, 可在我們這里根本不可能.
Q: 你們說你們之前的項目產品代碼和測試代碼的比例大約 1:3, 這不是平白增加了 3
倍的工作量嗎?
Q: 我們也做單元測試, 但是是先寫產品代碼后寫測試的. 難道非得 TDD, 非得測試先行嗎?