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