在靈活推進的過程中,一般認為有三大難點。
第一大難點就是故事拆分,我們的故事又要縱拆,又要拆小。縱拆就意味着橫跨整個端到端的流程,拆小意味着盡量要短。而且縱拆和拆小本身互相就是沖突的,是以覺得靈活推進第一難點就是拆分。
第二大難點,就是我們平時說的團隊建設。大家想一想,我們大部分都不是企業的股東,那我們有什麼力量去組織團隊呢?
第三大難點,就是我們經常說的tdd。要改變很多我們傳統研發的思維方式和觀念,同時又要牽涉到軟體設計方面的問題本質複雜度而這又是系統工程層面的東西。是以說,它本身的難度也是比較大的。 tdd就是too difficult to do,太難了,以至于我們沒法做。
實際上,業内以前認為是這三個單詞的縮寫,test driven development。但是随着時間的發展以及tdd的開展,tdd的本質不再是tdd,更多的是這三個單詞,test driven design,即測試驅動設計。因為tdd本質過程就是要貫穿從需求分析、設計、編碼、測試、整個研發過程,是以現在提的更多的或者主流觀念都是test driven design。 那我們接下來要談tdd,我們就要談它的前世和今生,前世就是tdd從哪裡來?

大家看一下上面的圖,這張圖對于熟悉靈活技術實作的同學可能比較熟悉。其實靈活流派比較多,目前有十多種。現在比較主流的就是scrum + xp 。scrum主要用于管理實踐,有階段的定義和管理的支撐,技術實作主要是xp - 極限程式設計。這個xp極限程式設計,主要由三個環組成。三個環裡面共有13個實踐 ,因為中間環的隐喻實踐使用不是很多,是以給出了常用的12個實踐。大家看,從外往裡面看這個洋蔥環,最外面的環叫做組織實踐環,這個是力度最大實踐環。是由四個實踐組成,當然四個實踐彼此是地位不相當的。其中有一種叫做核心實踐,其它叫做拉動實踐。核心實踐叫做,small release。隻有做到small release,才能快速實作商業價值、快速得到客戶回報,其它的測試、完整團隊啊都是為了small release服務的。
那第二層環,也就是中間層叫做團隊實踐環,力度相對于組織環小了一層。組織環是相對于産品層級的,而它是team層級的。實踐環也是有核心實踐和拉動實踐組成,大家應該都能猜到這一環的核心實踐就是穩定節奏。隻有團隊保持穩定的節奏,才能使團隊風險降到最低,脈沖和毛刺(指傳遞速度大幅波動)都會帶來極大不确定性。其它如持續內建、代碼規範都是為了支撐穩定的節奏而服務的。
好了,下面讓我們把目光集中到最内層的實踐環,也就是洋蔥的「芯」,它也由四種實踐組成:簡單設計、結對程式設計、tdd、重構。大家猜一下,哪個是核心實踐?好,同學應該都能答對,這一環的核心實踐就是「簡單設計」。它指對于要解決問題域的本質複雜度映射,比如我們要做一款軟體來解決某個問題,本身問題是有複雜度的,現假設問題複雜度為100。如果軟體要解決這個問題,很自然軟體的複雜度就要大于等于100才能hold得住這個問題。而這個軟體又是人寫的,人要了解和維護這個軟體。是以人腦的思維複雜度又要大于等于軟體的複雜度。這樣,如果我們一個設計能夠無限趨近于問題本質複雜度。使我們維護、了解軟體的成本最小,這種設計就是簡單設計,它是我們追求的目标。我們無論是做重構,做重構我們可以使用結對程式設計和tdd方式,這都是為了使解法逼近本質問題複雜度,也就是簡單設計。tdd就是我們圖中方塊的地方,它是我們實作簡單設計的手段。
這就是tdd的前世,也就是tdd從哪來的。
在靈活中所有的測試,單元測試、功能測試也好,都是黑盒的測試,隻不過單元粒度有大有小。
按照粒度的大小來分,一般來說tdd有以下幾種粒度組成。
這個粒度是ut,這裡的ut要和傳統的ut相區分,在靈活中所有的ut其實都是預設是黑盒ut,是功能單元的ut,隻不過功能粒度比較小,它本身非常内聚,麻雀雖小五髒俱全,都是基于對外功能接口上的,是以一定都是黑盒的,基于接口的測試。
粒度最大一層我們一般把它叫做ft,function test。針對前面的ut來說,到這裡是指子產品級了,粒度更大一些,是把n個ut串起來。
現在引入bdd,其實從本質來說是一種tdd展現形式,隻不過它的粒度更大,從使用者行為的角度來進行測試。
是一種驗收測試,系統需求級别的測試。tdd本身是分層的,ut、bdd這些本質來說沒有差異,都是對接口的測試對功能的映射進而驅動開發,我們可以叫做xdd。
tdd和傳統ut之間的差别
下面我們講講tdd和傳統ut之間的差别,傳統ut,最大的問題一般都是事後編寫, 這樣我的用例會遷就我的代碼。遷就的意思就是,生産代碼已經寫好或者在某種壓力上針對生産代碼做一些白盒的單元測試,因為代碼已經産生,也不可能廢除或者删除掉。隻能用例遷就代碼,如果代碼耦合、灰色,用例自然也就很耦合和、灰色。那這種遷就化會造成測試白盒化,白盒測試最大的要命問題就是造成用例不穩定!
我們都知道,從穩定性角度來說,需求和實作相比,需求相對穩定,實作相對變化就比較大。比如換實作方法或者性能不達标要重構。針對穩定性來說,一般需求相對穩定,那測試白盒化需要我們把用例寫在實作上,代碼有多少分支就寫成用例。實作相對需求又不穩定,是以實作一改變就會導緻用例變化,進而用例很難維護。
做過傳統ut的團隊可能都會有這種體會,當年我所在的團隊也做過傳統ut,用例太容易變化。以至于我不得不花額外的人力維護用例,導緻很多團隊堅持不下來。用例白盒化造成第二個問題就是需求脫節,我們知道一個需求它被實作之後,是通過很多代碼邏輯組合起來的,如果用例寫在代碼邏輯上,就很難和明确的需求相對應,造成用例和需求脫節。一旦某個用例不通過,很難和明确的需求對應起來,說不清哪個需求受到影響,這也是用例很難維護的原因之一。第三,測試白盒化造成用例成本效益相對不高,我們也做過試驗,如果要想達到60%的分支覆寫率。采用完全的白盒化方式,用例的代碼行數和生産代碼的行數要達到2:1左右的比例。大家看為了達到60%分支覆寫率,要付出兩倍的測試代碼的代價,這個成本效益是相對不高的。很多開發人員為什麼對ut有反感和抵觸也是這個原因造成的。因為我除了要維護生産代碼以外,還要維護測試代碼。
采用tdd方式以後,把用例寫在需求上、接口上、場景上,通過用例把一個個白盒邏輯串起來,就像一個珍珠項鍊有一條線把所有的珍珠串起來。這樣同樣達到分支60%的覆寫率,用例的代碼數量和生産代碼相比大幅度下降,甚至幾倍的數量級。是以,白盒用例往往會造成用例非常複雜,因為是針對實作嘛。我曾經自己做過一些碼流,就是二進制碼流。針對協定棧碼流的單元測試,構造用例就要構造成二進制,如果沒有适當的第三方工具和自研工具,碼流構造會非常複雜而且很難看懂和難以維護。跑出來的用例也很難定位,它非常複雜、耦合和晦澀。
是以以上種種往往用例成了一個重構的障礙。人心裡都是這樣,我們經常講沉沒成本,就是說投出努力後付出再也收不回來了,就會一般很難放棄。這個白盒用例也是這樣,我們花了這麼的代價,剛才上面說的為了達到60%的分支覆寫率測試代碼和生産代碼2:1的比例,這麼大的代價已經付出了。可重構,就意味着我們不改變系統外在的行為而要改變内在的實作。那我的用例就是寫在内部實作上的,是以我大量的用例跑不通過。如果放棄這些已經完成的用例,自然我很心痛。是以我不願意去重構。
剛才我們講tdd和傳統的白盒用例的差别。當然了,由于白盒測試這麼多問題,也是促進我們進一步開展tdd的動力之一。那下面就和大家分享一下tdd怎麼做,是不是too difficult to do?其實它還是有一定的方法和脈絡的。那tdd的本質,我了解就是需求分析,這也對應我本次的分享,tdd的本質不是tdd而是需求分析。
是以,我們在做tdd之前,首先拿到需求,之後對需求進行分析,分析過程就是把需求按照故事來拆分,然後故事按照場景進行分析,然後每個場景執行個體化。比如我有一個接口,接口有傳回值,傳回值具體執行個體化成0和1。這樣需求就會被拆解開來。需求拆解開來之後,針對需求的每個故事,故事的每個場景來寫用例。
這個時候有兩種方法,第一種,先設計出接口然後寫用例。另一種先寫用例然後再定義接口。大家看一下,覺得哪種方法更合适一點 ?答案這樣的,我們推崇針對拆分好的故事和場景先寫用例,先用這個用例決定接口什麼樣子,然後再把接口定義出來。用例針對接口而寫,而接口在某個場景下就是代表需求的,進而我的用例就是可以代表需求的 。
通過編寫用例再把接口定義出來,這種方式我們做過實際的測試 這是一個統計資料。如果分支覆寫率達到一定目标,和白盒測試相比,測試代碼和生産代碼會下降三到四倍。是以說針對需求開發用例成本效益會提升。
tdd的做法就是六個字,我們常說的六字真言:紅色、綠色、藍色。紅色的意思就是說我把這個需求分析完之後,先把需求進行實作。主要是為了驗證接口設計對不對,并沒有做具體實作,這個時候編譯通過後一跑就是紅色。第二個快速實作,就是把很多複雜的政策寫死,比如直接return 0或者return 1,繞過去複雜的政策。這個時候,它一跑結果就是綠色。那有同學說了實作這種綠色有什麼意義呢?其實一次貫穿的用例至少代表一種業務場景,可以去驗證編譯和語義環境。第三個藍色,就是說把用例快速實作然後果斷重構,重構過程看看是不是命名充分表達使用者語義,橫向排版和縱向排版是否整齊,去掉多餘的空格和換行使得語義更緊湊,檢查邏輯是否可以更順暢,把所有的重複都消掉,使我們的代碼變得海水天空一樣湛藍,這個我們叫做藍色。然後,變藍後我再去實作下一個場景,然後再去重複這個過程,紅色、綠色、藍色。那有的同學可能會問,為什麼我不能一步把它變成綠色或者一步變成藍色?為什麼還要有一個變紅的過程那?這個地方,我們先埋下一個伏筆,後面我們會重點講述這個問題。
接下來我們談一下tdd的特點,那第一個特點就是驅動開發。
為什麼叫做驅動開發,驅動開發有鞭策、促使、推動的作用,其實是一個主動詞。為什麼叫它主動詞,因為我們先對需求進行拆分,拆分成一個故事然後按照場景拆分,然後針對場景寫測試用例,針對測試用例使用接口定義接口原型。這個原型包括接口本身,包括接口事前條件就是說滿足調用條件,然後包括事後條件。這就是接口的原型事物的三要素。最後才是針對接口的一個實作并跑通用例,是以大家看所有的過程都是需求驅動開發,而不是傳統的對需求還不是很了解的情況就去開發,然後再去測試,這有一點開發驅動需求的感覺。這就是tdd裡面測試驅動開發的第一層含義。
第二層含義,就是說我們不僅需求能驅動開發而且要剛剛好,just in time 。那什麼叫做剛剛好,就是說用例設計完後,代碼去實作,跑通用例後用覆寫率工具去檢視,發現所有的代碼都對用例有貢獻。在我場景拆分足夠細緻詳細的情況,我們發現某些代碼對用例沒有貢獻,就認為這個代碼是備援代碼。是以第二層含義就是不僅驅動,且實作的剛剛好。
tdd的第二個特點就是改善設計。我們經常看到網上有一副漫畫,客戶眼中的程式員 都是外星人的樣子,鼻子長在腦袋上,皮膚是綠色,說的外星語,和她很難溝通,這就是客角度他眼中的程式員的樣子。同樣,程式員眼中的客戶是什麼樣子呢?從程式員眼中看客戶,都是穿着獸皮裙扛着大木棒頭發亂糟糟的野蠻人形象,毫不講道理,程式員提供的接口,他随意調用接口,粗暴的調用。
為什麼會有這種現象?因為大家都站在自己角度看問題。tdd要求程式員先看需求再去實作,強制程式員先把屁股挪到客戶那裡。從客戶的角度來看待接口設計的問題,這樣往往能發現很多深層次設計問題。比如你先寫用例然後定義接口,很容易發現接口過度依賴點的問題。也叫作依賴過多問題,比如說我有一個三角形的類,有一個接口可以get三角形的底,另一個接口可以get三角形的高,然後客戶get到三角形的底和高後,再用底乘以高除二計算三角形的面積。大家猜猜這個計算三角形面積的調用,對我的三角形類有幾個依賴點。其實是三個依賴點,除了get底和get高, 還用到一個三角形知識:底乘高除二等于面積。是以說這個接口設計不合理,存在過度依賴的問題。是以我們希望把這個接口進行修改,增加一個計算面積的接口 - trianglearea。
與前面相比有什麼特點,首先接口的數量減少了。我們說到接口本身背後的知識,如果一個子產品依賴另一個子產品過多,那另一個子產品變動,則該子產品變動的機率就會增大,造成波及影響。同理,第二個那個針對get底、get高,計算三角形面積來說,擷取面積接口trianglearea更穩定。比如說計算三角形不再是底乘高除二,而是兩個臨邊乘夾角除二。則後面這個接口就不用變化,是以這個接口更穩定。綜上,從使用者角度來看,這個接口是否造成依賴過度。
第二個,從客戶角度來看接口,還容易看出接口的功能是不是單一,接口是不是易錯。比如說,我們經常講的c語言stringcopy函數,一定是destination放在前面source放在後面,如果設計的接口把source放在前面而destination放在後面,對于一個熟悉c的接口人員就非常可能調用錯誤。
第三個,看看接口是不是有第三方依賴。我們碰到很多的接口用起來非常不爽,因為裡面有第三方依賴,還要自己去管理。沒有對必要的第三方依賴進行抽取,這個也有利于審視你的接口。是以tdd對改善設計是非常有利的,做tdd确實有一些思考,比如說設計上更本質更背後的一些非常有意義的東西。
第三個特點就是可以快速回報。在tdd中,所有的用例都可以積累起來而且還是自動化的,是以有問題可以及時發現快速回報,可以快速形成一個保護網。tdd我們剛才講了驅動開發,不僅實作需求而且僅僅實作需求,可以消除過度開發和過度設計。快速回報的另一個作用可以激發你進一步做一件事,比如說在遊戲裡面打怪或者做某一件事可以馬上獲得經驗值或者寶物,這個可以刺激人的多巴胺神經,讓你繼續去打怪或者做某一件事情。其他還有很多這種特點,比如說提升用例的表達力啊,這裡就不一一累述了。
然後第四點,tdd可以起到一個需求文檔的作用,對場景描述表達力非常強。經常使用三方工具看說明文檔不一定能懂,但是看用例,把用例抄一遍一般都可以直接運作的。是以說,tdd非常強大抽取非常好的時候,确實可以起到一個文檔的作用。
第五點就是可以生成一個保護網,可以快速對基礎功能進行回歸,一般tdd都是ut、ft這樣的,運作時間比較短回報速度比較快,起到一種保護好的作用。
下面我們分享一下tdd裡面的關鍵點,就是tdd有一些步驟是很關鍵的,這一步我們是不能跳開的。
tdd也有這種作用,我們叫做small win。我們用例紅色,然後快速實作綠了,然後快速重構,又綠了。心裡多麼刺激,然後不停的小步快跑,然後不停的回報通過這種刺激去做tdd。
做好這個獨立性,做好隔離就很容易就能定位代碼的問題。
以上就是分享就是我們做好tdd的關鍵點,下面我們看一下做好tdd都有哪些挑戰。
第一個叫做small step,小步快跑,因為tdd本身要做好前面講要有一個small win。通過小步修改,小步送出、小步周遊,進而獲得小步勝利的感覺,會獲得一種節奏感。大家是否跑過馬拉松,我本身在南京包括去年都有參加,跑馬拉松最重要的就是節奏感,一旦有節奏感就不容易累。tdd的好處就是獲得這種小步快跑的節奏感。
另一個提倡no debug,就是不提倡debug。就是通過這種小步快跑上一步是綠的,又送出代碼又是綠的。如果說某一個小步變紅了,因為我的步子小嘛,我就很容易看出問題,這樣我也不需要debug。如果看了半天,實在發現不了問題,有可能兩種原因。第一你的代碼太複雜;第二個就是你的代碼跨度太大了;步子大怎麼了,容易扯着*,是以我們一定講究small step。如果在步子很小的時候,實在是通過眼睛走檢視不出問題,那我們就回退,回到上一個階段,重新一小步一小步走下來。
tdd第二個關鍵點就是要做到依賴隔離,因為我們知道很多的代碼跑起來要依賴外部的環境、配置檔案、甚至服務,才能跑起來。做tdd時候我們希望快速回報,是以外部的依賴我們要撥開。這也是tdd的難點之一,我們一般可以通過mock/stub方法來進行一些依賴的隔離。由對這個hardcode硬代碼依賴變成抽象接口資料類型這種方式的依賴,由依賴細節到依賴穩定抽象。然後做測試,通過依賴追蹤注入到到生成代碼中,這樣我的用例就可以跑起來了。
下面一個關鍵點是黑盒,用例一定要寫在接口上。接口暴漏的行為,就是我用例分别實作的場景。一旦用例跑不過,我能很快知道是哪個功能受到影響。
第四個就是獨立性,用例獨立性。一個用例跑不過之後我可以很清晰的知道是用例的問題還是第三方依賴問題。這也是剛才上面為什麼講要做依賴隔離。很多時候系統上線出問題後,我們很難界定是第三方問題還是自己代碼問題 ,有可能就是第三方問題,但是錯誤實在代碼中,是以說很難界定到底責任實在哪。如果我的代碼都跑過了,上線後有問題那,那80%可以确定是第三方依賴問題。
最後一個要點叫做非侵入,就是我設計用例或者設計接口的時候,不能為了測試而增加分支或者編譯條件。這個時候會對生産代碼造成一定的傷害,我們也吃過很多這方面的虧。是以一定通過依賴隔離的方式,把測試分離隔離開,而堅決不采用這種傷害的方式。
下面留三個小問題,同學們自己思考一下!
第一個問題,是否所有代碼都需要tdd?
第二個問題,tdd是否會降低效率?
第三個問題,tdd是否對團隊技能有要求?
問題一: 采用bdd的測試架構,還是xunit的架構,更适合做tdd?
回答一: 在coding層面,用xunit可以快速獲得回報,啟動small step和small win的作用;在特性和需求方面,用bdd或atdd,獲得就需求的回報,結合起來
問題二: 您好,請問對與舊系統的維護使用tdd有哪些建議呢?
回答二: 新增功能和bug修複可以采用tdd的方式,對3方遺留或舊系統遺留可以采用依賴隔離的方式。讓開發人員感受到節奏感,然後逐漸擴大tdd的範圍
問題三: 如何在團隊内推動tdd,并使其長期有效,有沒有好的經驗分享?
回答三: 管理上,采用吸引的方式,邀請tdd熟手經常插花和生手結對,幫傳帶;技術上,開發tdd架構,進行依賴隔離,讓tdd很容易開發。
tdd是一種軟體開發的方法,軟體設計能力是系統工程論的範疇。可以在不同的設計能力做能力想比對的tdd,随着tdd的開展,又可以促進軟體設計能力的提升。
問題四: 你工作過程中有多少是采用tdd模式開發?實作推動時需要哪些層面的支援?
回答四: 自己帶的團隊都是采用tdd開發,我當時每周抽2天每天2個小時找不同人結對,以點帶面,形成氛圍。
<b>分享者簡介:</b>
丁輝,中興通訊公司級靈活教練和代碼大全、代碼設計訓練營教練,12年軟體開發經驗,8年項目管理和流程改進經驗,指導并參多個團隊由傳統研發模式向靈活研發模式轉型(其中超過100人的大型團隊成功項目級靈活轉型5個),在靈活導入、指導團隊轉型、ci、核心技術實踐、自組織團隊建設等方面具有豐富的實戰經驗。
中生代技術群微信公衆号