<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    posts - 56, comments - 77, trackbacks - 0, articles - 1
      BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

    敏捷質疑: TDD

    Posted on 2008-07-13 21:40 切爾斯基 閱讀(4732) 評論(4)  編輯  收藏

    Q: 為什么通過單元測試發現的 Bug 很少 ?

    A: 單元測試不是用來發現 Bug 的, 而是用來預防 Bug 的. 如果采用 TDD, 測試用例完成之時, 產品代碼尚未編寫, Bug更無從談起.

    Q: 那是否寫單元測試就能提高代碼質量了 ?

    A: 關于這一點, 似乎有人不這么看, <<TDD Opinion: Quality Is a Function of Thought and Reflection, Not Bug Prevention>>. 不錯, 代碼質量并不必然關聯到單元測試, 諸如凈室軟件開發之類的方法依然可以在沒有單元測試的情況下得到高質量的代碼, 但這是另外一個問題. 或許主觀上, TDD的本質更接近于促使你把質量內建在思維中, 但客觀上, 在其它條件都相同的情況下, 單元測試依然能夠起到預防 Bug 的作用.

    Q: 單元測試怎么能反映/代替需求 ?

    A: 單元測試未必能直接反映宏觀上的需求, 但

    1. 功能測試和集成測試能夠反映宏觀需求.

    2. 單元測試能夠反映系統的其它部分對當前單元的需求.

    而從文本的角度, 測試用例的名字就是需求的描述. 換句話說, 你從傳統的需求文檔中把描述摳出來, 放到測試代碼中作為測試用例的名字, 你便擁有了可執行的需求文檔

    一個 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 { //直接業務需求

            ...

        }

        public void testShouldThrowExceptionIfCannotFindConfigFile() throws Exception { //來自系統其它部分的需求

            ...

        }

    測試用例并不排斥業務層面的需求文檔, 一個高層的, 突出業務價值的需求/愿景描述對于快速理解系統是非常有幫助的, 但/只是測試用例以另一種方式描述了真實的系統, 它具有兩個突出的優點:

    1. 它不會說謊, 即永遠與系統真實的行為同步

    2. 它是可執行的, 它可以不知疲倦的, 成本極低的, 時時刻刻, 反反復復的追問你的系統是否符合需求

    Q: 需求變了怎么辦? 豈不是有大量測試用例需要修改?

    A: 難道不是應該的嗎? 難道以前的需求文檔在需求發生變化時不需要修改?  哦, 或許它們不需要, 因為沒人會關心, 對代碼也沒什么影響, 需求文檔在度過最初的幾周后便被扔在配置庫里再也沒人管它了.

    是的, 這關系到你的測試策略. 然而通常的測試策略對單元測試的要求都是盡可能周全. 于是這就是一個測試設計的問題. 是的,測試代碼也需要設計, 也需要重構, 也需要 Domain Specific.

    Q: 我的單元測試編譯鏈接速度很慢, 而且有些條件很難測, 比如內存不足, 或者環境很難搭建, 比如需要網絡或數據庫, 怎么解決?

    A: 這是集成測試, 不是單元測試. 你一定把系統所有的組件都編譯鏈接起來了. 那么如果你的測試失敗了, 是哪一部分的問題呢?

    通常單元測試需要滿足一個條件: 不依賴任何其它單元, 即隔離性. 實現手段就是在測試環境中能夠輕易的假冒依賴, 并設定依賴按照我們的意愿進行工作. 一個例子就是你的代碼依賴 malloc 獲取內存, 而你想測試內存不足的情況. 那么我們應在能夠/需要在單元測試中使用使用一個假冒的 malloc 來代替真正的 malloc, 并且我們能控制假冒的 malloc 返回 NULL 以模擬內存不足的情況. 關于如何做到這一點, 可參考一些成熟的"假冒"框架, 如 mockcpp 等.

    Q: 我原來的測試都是用真實的代碼來跑, 一個測試能覆蓋多個單元. 你現在都把依賴替換掉了, 那被替換掉的模塊有問題怎么辦? 怎么保證集成真實的代碼后還能正確工作?

    A: 其它單元有其它單元自己的單元測試, 各自關注自己. 集成測試像以前一樣, 該怎么測還怎么測, 并不是有了單元測試就不要其它測試了.

    Q: 單元測試就是設計? 單元測試怎么能反映/代替設計 ?

    A: 單元測試反映的是局部的設計, 局限于本單元以及與之交互的其它單元. 前面說的單元測試能夠反映系統的其它部分對當前單元的需求, 所謂設計就是單元之間的職責劃分, 交互和依賴關系

    當你試圖測試一個單元時, 卻發現需要創建大量的其它對象, 而且按照你腦海中的實現, 有些對象是在單元內部創建的, 根本無法在測試環境中假冒它們. 這時候, 你即使只是為了減少測試的難度, 也會逼迫自己思考:

    1. 這個單元是否做了太多的事, 承擔了額外的職責, 違反了單一職責原則?

    2. 是否應該把依賴讓外界設置進來, 而不是自己在內部創建, 這樣測試時就能把依賴設置為假冒的實現?

    是的, 單元測試警示你思考一下自己的設計

    Q: 單元測試是設計, 還有人說源代碼是設計, 到底是測試是設計還是源代碼是設計?

    A: 這實際上是另外一種角度. 源代碼就是設計的論斷基于兩個假設

    1. 設計階段中工程師的工作產物, 也就是他的設計, 是應該能夠在實施階段被不同的實施者嚴格并且幾乎一模一樣的實現

    2. 軟件開發人員也是工程師, 即軟件工程師

    如果我們認同這兩個假設, 那么軟件工程師的什么產物能夠被嚴格并且重復實現的呢? 是你的Word形式的"設計"文檔嗎? 是CAD工具畫出的UML圖嗎? 都不是, 因為它們都不精確, 有無數種實現方式, 根本談不到嚴格, 不同的開發人員會有完全不同的實現. 事實上, 只有源代碼,才能滿足這個約束. 這樣軟件的設計階段, 就是直到軟件工程師完成源代碼的那一刻, 而軟件的實施階段, 其實就只剩編譯和部署了. 跑題了.

    Q: 單元測試是需求"文檔", 單元測試又是設計"文檔", 它怎么能既是需求又是設計呢?

    A: 名字和斷言描述需求, 環境設置描述設計 ...

    Q: 既然單元測試描述的是需求, 它就應該是黑盒測試了? 可單元測試不一直都被認為是白盒測試嗎?

    A: 黑白都是相對于你觀察的層次. 相對于其它從外部觀察"系統"行為, 不涉及源代碼的測試來說, 單元測試深入到內部觀察盒子的行為, 所以是白盒. 而具體到每個單元測試用例, 依然在盡可能的從外部觀察"單元"的行為, 所以又是黑盒.

    Q: 但是你們常用的 Mock 技術, 明顯把單元測試推向白盒的境地.

    A: 說來話長, 但可以先說結論: 基于狀態的測試 over 基于交互/行為的測試, 雖然右邊的也有巨大的價值, 但我們認為左邊的更穩定和更富有對系統的洞察力

    基于狀態的測試描述的是需求, 基于交互行為的測試描述的是實現. 相對于需求來說, 實現更易發生變化, 尤其在另外一種實踐"重構"的沖擊下, 描述實現的測試將被修改的面目全非, 帶來相當的返工和維護成本

    一種例外, 就是交互本身就是需求, 這時 mock 是合適的選擇. 一個杜撰的例子參見<<TDD: Tricky Driven Design 3, 方法>>中最后銀行API的例子

    而現實生活中, 存在一些情況, 雖然使用 mock 可能帶來后期的維護成本, 但它帶來的好處也是不可代替的. 比如對先期整體測試代碼的編碼量的降低. 這在 C/C++ 項目中尤其明顯:

    受限于C/C++的編譯模型, 使用常用的預處理期接入點和編譯鏈接接入點技術來接入 stub 實現時, 要小心維護頭文件的防衛宏, 頭文件的名稱, 不同環境下構建腳本的include路徑設置, 庫路徑設置等. 手工寫stub的方式變的及其繁瑣和容易出錯. 這時候, 一個易用的 mock 框架如 mockcpp 等將節省大量的編碼和先期維護工作

    而幾乎所有的mock框架, 都支持將 mock 對象退化為 stub, 如 mockcpp 中 mock 對象的 defaults() 設置, 或者 JMock 2 中的 Allowing . 事實上, 這是我推薦的 mock 使用方式: 通常情況下讓它退化為簡單的stub, 必要時才使用它強大的期待設置和驗證能力.

    通常單元測試有兩個公認的約束需要滿足:

    1. 隔離依賴.

    重申一遍結論就是: 在滿足單元測試的快和隔離依賴的前提下,

    1. 優先選擇基于狀態的黑盒測試(可使用手寫stub或mock退化的stub)

    2. 除非交互和行為本身就是需求(可使用mock對象的全部特性)

    Q: 怎么測 private 函數?

    A: 把它變成 public 的.

    我是認真的. 如果發現 private 函數無法簡單的通過某個public函數的測試來覆蓋而需要專門的測試, 意味著你的單元可能承擔了太多的職責, 應該拆分到一個單獨的單元中, 并開放為 public 函數.

    如果使用 C++, 在測試環境中 #define private public.

    如果使用 g++, 在測試環境中加入 -fno-access-control.

    Q: 類似 private, 一些意圖實現良好設計的語言特性, 如 static, sealed, final, 非虛函數等, 卻總是給代碼的易測試性帶來麻煩, 該如何取舍?

    A: 沒什么好辦法. 這些語言特性和測試的目的是相同的, 都是為提高代碼質量, 減少出錯的可能, 雖殊途同歸, 但卻互相限制, 效果也不一樣.

    我認為工業界是時候嚴肅認真的考慮測試環境了, 最好在語言中內建對測試的支持, 一些為產品環境設計的語言特性, 應該在測試環境中關閉, 而在產品環境中生效. 其實之前很多編譯器都支持 Release 和 Debug 兩種環境, 也是從代碼質量的方面考慮的. 現在毫無疑問證實單元測試比 Debug 更有效, 是時候與時俱進增加對 Test 的支持而逐漸罷黜對 Debug 的支持.

    在語言本身增加對測試的支持之前, 我們不得不想辦法在測試環境中繞過語言特性的限制, 尤其對遺留系統, 代碼已經存在的情況. 比如對于 C++ 中的 static 函數, 可以將整個被測單元 #include, 或者 #define static 為空. 宏代表了一層間接, 在測試環境中, 這層間接是至關重要的. 其它方法可參考 <<Working Effectively with Legacy Code>>, <<假冒的藝術>>中的介紹.

    Q: 剛才提到了要支持"測試"而不是"Debug", 測試和Debug難道有什么矛盾嗎?

    A: 有. 如果你發現不得不 Debug, 就是測試粒度太粗, 步子邁的太大, 產品代碼過長等導致的, 甚至可能你卷入了過多的單元而破壞了測試的隔離性. Debug還是代碼邏輯不清, 行為難以斷言的表現.  用測試幫你定位錯誤.

    Q: 我知道為遺留系統增加新特性是要先寫測試保證系統原來的行為, 可遺留代碼很龐大, 我甚至都不知道系統目前的行為, 怎么辦?

    A: 特征測試: 保持代碼行為的測試, 獲取當前運行結果, 來填充測試, 以獲取系統目前行為. 其實測試可以分為兩類: 試圖說明想要實現的目標, 或者試圖保持代碼中既有的行為; 在特性實現后, 前者會轉化為后者. 詳細信息請參見<<Working Effectively with Legacy Code>>

    Q: 有成熟的關于在遺留系統上實踐 TDD 或者單元測試的實踐嗎?

    A: 還是<<Working Effectively with Legacy Code>>, 或者<<在大型遺留系統基礎上運作重構項目>>

    Q: 前面經常說到 C++ 或其它面向對象語言, 卻沒有提到 C, 那么過程式語言中如何應用 TDD ? 有什么不一樣?

    A: 基本一樣, 并且在過程式語言中應用 TDD, 可能會導出面向對象風格的設計. 比如如果直接調用某個函數, 那么不得不通過編譯時替換或鏈接時替換來接入假的實現. 這樣其實比較麻煩, 因此可能會促使你選用函數指針 ,以便方便的在測試環境中進行替換. 隨著時間的推移, 你會發現一組組概念相關的函數指針出現了, 那么把它們和它們操作的數據綁定在一起, 定義一個 struct, 就形成了一種對象風格. 當然這反而可能會令你的代碼更復雜, 這需要在實踐中取舍.

    也有可能在過程式語言中你覺得 TDD 對設計的促進不大, 而且測試用例也比較枯燥, 就是測個分支, 返回值什么的. 是的, 邏輯就隱藏在分支和返回值中, 如果習慣了過程式思維并不打算改變, TDD 對設計的影響則更多的體現在依賴管理上, 如頭文件和編譯單元的職責劃分. 如果把不同職責的函數混在一個編譯單元里面, 則很難實施鏈接替換等手段, 除非你選擇一個類似 mockcpp 的框架, 不需要鏈接替換.

    Q: 如果使用 TDD, 那么測試人員怎么安排? 是不是一開始就要進入項目組? 可那時還沒有產品代碼,測什么?

    A: 是, 是一開始就要進入項目組, 可不是因為 TDD.  是, 測試人員是一開始沒什么可測的, 可不代表就沒活干.

    TDD是一種開發方法, 是開發人員參與的活動. 其效果是以可執行的形式文檔化你的需求, 迫使你分清職責隔離依賴以驅動你的設計, 編織安全網以扼殺Bug在搖籃狀態防止逃逸. 可傳統測試人員的活動是試圖找到已經逃逸的Bug. 這兩種活動都是必要的, 而且毫不沖突, 互為補充.

    那么測試人員在新的特性還沒開發完成之前做什么呢? 除了提前寫測試用例, 無論是自動化的還是非自動化的, 而需要測試人員參加的一項重要活動, 就是參與特性驗收條件的制定. 之前經常發生開發人員按照自己的理解去編碼, 測試人員按照自己的理解去測試, 直到開發完成, 測試過程中才發現理解的不一致, 開始產生爭執并阻塞等待業務分析人員(如果幸運的話)或者行政主管(如果開發過程混亂的話)的仲裁. 解決辦法就是就在開始開發新特性前的一剎那, 由業務分析人員, 測試人員, 開發人員進行一次討論, 就驗收條件達成一致并形成記錄, 然后測試人員和開發人員分頭去寫測試和實現.

    Q: 之前會有一個階段, 就是一組相關的特性開發完成后, 測試人員接手測試, 幾輪Bug修復過去后, 產品基本穩定就可以發布了. 現在測試人員提前介入到每個迭代中, 針對單個特性進行測試, 那如何保證產品集成起來的質量?

    A: 跟以前一樣, 該有那么個集成測試階段還得有那么個集成測試階段, 取決于產品當時的質量狀態. 并不是說有了迭代級別, 單個特性級別的測試就不需要發布級別的集成測試了, 兩者沒有任何矛盾.

    Q: 那么測試人員提前進入迭代有什么好處?

    A: 盡早發現問題, 降低修復錯誤的成本. 有幾種手段, 一是前面提到與業務人員和開發者一起討論驗收條件, 這樣就能防止理解偏差而導致的返工. 二是開發完成立即測試, 發現問題立即反饋, 這樣開發人員對代碼依然印象深刻,能快速定位和修復錯誤. 這樣流入最后集成測試階段的Bug就會少, 會縮短最后的集成測試時間, 保證產品更平穩的發布.

    Q: 有時候后續的特性會影響前面的特性, 那么迭代過程中測試人員只測單個特性, 怎么保證以前的特性依然工作?

    A: 幾個手段. 測試盡量自動化, 以便能夠持續集成. 再就是做好依賴管理, 每當一個新特性完成, 就應該能夠發現它影響的其它特性, 看看是否應該補充一些集成測試.

    Q: 有時候開發人員完成一個特性時已接近迭代結束, 測試人員沒有時間進行充分測試, 怎么辦?

    A: 下個迭代測唄, 并且在計算開發速度時, 只應該計算本迭代通過測試人員驗收的特性, 那些僅僅是開發人員完成, 沒有經過測試人員充分測試的特性不計在內. 這種情況是不可避免的. 但我們能通過一些手段讓測試與開發更加同步, 盡量縮短滯后性, 包括讓測試人員與開發人員更緊密合作, 盡量讓測試用例自動化等.

    Q: 我還是覺得在開發迭代過程中, 測試人員的工作量不飽滿.

    A: 如果這不是您的感覺, 而是事實, 并且前面測試人員必須要做的工作也都做了, 還是不飽滿, 那么恭喜你, 可以省下一些測試人員, 去做別的事了. 但不推薦的是, 不要讓測試人員同時為兩個團隊工作. 這會大大增加溝通的成本. 你會經常發現, 當你的開發者想找測試人員協助時, 卻找不到人了, 于是你的團隊便被堵塞在那里. 而測試人員本身的Context切換也是痛苦的.

    Q: 你們說驗收測試應該由客戶來編寫, 可在我們這里根本不可能.

    A: 驗收, 當然是由客戶來驗收, 這在理論上是毫無疑問的, 而且肯定在各行各業發生著. 只是具體到測試用例的編寫和執行, 無論是自動化的還是非自動化的, 都需要掌握一定的技術, 需要周密的思考, 需要專門的時間, 客戶可能無法同時滿足這幾個條件, 我們要盡力爭取, 爭取不到, 便只好通過更充分的交流來彌補越俎代庖的失真. 這時業務分析人員和測試人員要通力合作, 完成驗收測試的編寫.

    Q: 你們說你們之前的項目產品代碼和測試代碼的比例大約 1:3, 這不是平白增加了 3 倍的工作量嗎?

    A: 是增加了 3 倍的代碼量而不是工作量. 它節省了你幾十人做幾個月龐大的預先設計的工作量, 節省了你詳細設計每個模塊并為之編寫幾百頁詳設文檔的時間, 節省了無數不眠之夜通宵Debug的時間, 它節省了集成階段修復難以計數的Bug的工作量, 甚至它縮減了你產品代碼的數量, 大量的重復代碼被消除了, 大量過度設計的復雜代碼被廢除了, 你的代碼更易理解了, 添加新特性更容易了, 發現的Bug更易定位了, 以致于大大減少了長達數年的生命周期內維護的工作量. 有點夸張了? 可這就是 TDD 和敏捷開發帶給我們的好處(如果你已經實踐了)和vision(如果你還在觀望)

    Q: 我們也做單元測試, 但是是先寫產品代碼后寫測試的. 難道非得 TDD, 非得測試先行嗎?

    A: 沒什么事是非做不可的. 取決于你要什么. TDD 只是以可驗證的方式迫使你將質量內建在思維中, 長期的測試先行將歷練你思維的質量. 而事后的單元測試只是惶恐的跟隨者.


    評論

    # re: 敏捷質疑: TDD  回復  更多評論   

    2008-08-20 15:48 by rocket
    應該再加一個:
    Q:UT是測試接口還是測試實現?
    A:UT測試的是代碼,是為了讓代碼正確的工作,所以你要測試接口的話,那就必須要接口里可以寫代碼:)

    # re: 敏捷質疑: TDD  回復  更多評論   

    2008-09-18 18:12 by sp123馬甲
    加了這個就必須再加一個:
    Q: 是否先寫實現代碼然后寫測試代碼?
    A: 不是這是基本的TDD概念問題哦!測試先行,測試的整個流程都可以針對接口,只有第一句實例化被測試對象時才需要針對實際的實現。即使如此,測試寫下來之后,你無法編譯通過,這正好是TDD的第一個動作,你需要補充一個實現來讓編譯通過!

    # re: 敏捷質疑: TDD  回復  更多評論   

    2008-12-18 21:12 by lilee
    寫的很好,可否轉載?

    # re: 敏捷質疑: TDD  回復  更多評論   

    2008-12-18 21:25 by chelsea
    @lilee

    注明出處就可以.

    只有注冊用戶登錄后才能發表評論。


    網站導航:
     
    主站蜘蛛池模板: 57PAO成人国产永久免费视频| 免费无码又爽又高潮视频 | 全黄性性激高免费视频| 成人自慰女黄网站免费大全| 亚洲av无码专区国产乱码在线观看 | 亚洲爆乳无码专区www| 亚洲视频一区二区| 99精品在线免费观看| 天堂亚洲国产中文在线| 亚洲夜夜欢A∨一区二区三区| 免费观看激色视频网站(性色)| 亚洲av无码专区在线电影| 亚洲韩国精品无码一区二区三区| 精品免费久久久久久久| 永久免费精品影视网站| 亚洲黄色片免费看| 亚洲AV无码一区二区三区在线观看 | 三级黄色免费观看| 亚洲AV无码成人专区| 久久久青草青青国产亚洲免观| 亚洲成人免费在线观看| h在线看免费视频网站男男| 亚洲伊人色一综合网| 亚洲精品成人无限看| 日韩毛片免费在线观看| 91制片厂制作传媒免费版樱花 | 亚洲精品无码成人片久久 | 精品免费久久久久国产一区| 亚洲AV无码第一区二区三区| 免费一级毛片在线播放不收费| 玖玖在线免费视频| 精品特级一级毛片免费观看| 国产亚洲精品岁国产微拍精品 | 久久亚洲AV午夜福利精品一区| 大陆一级毛片免费视频观看| 久久久久免费看成人影片| 精品成人一区二区三区免费视频 | a级毛片毛片免费观看久潮喷| 亚洲国产日韩精品| 亚洲精品中文字幕无码AV| 亚洲一级Av无码毛片久久精品|