推行編程利器之一TDD的思考
我在參與的開發(fā)項目以及咨詢項目中,都有實踐TDD的經驗。直至今日,我仍然會在某些功能開發(fā)時采用TDD的方式實現(xiàn)功能。雖然沒有達到將TDD溶于開發(fā)血液之中形成自然而然的習慣,但至少也是我常用的編程利器之一,偶爾使用,效果還算不錯。
以下內容則是我在某大型團隊中推行TDD時的一些思考。當時的整個咨詢過程,至少在TDD推行上可以稱得上是舉步維艱。如今看來,這些思考仍有現(xiàn)實意義。
1. 開發(fā)人員的質量意識
開發(fā)人員包括管理人員的軟件質量意識,常常立足于清晰可見的外部質量。評價一個開發(fā)人員的績效,很重要的一個指標就是被測試人員發(fā)現(xiàn)的缺陷數(shù)。
慣常的軟件開發(fā)思想,總是認為開發(fā)人員不適合做測試,因為他們總是站在自己的角度去看待問題,從而可能忽略真正需要測試的用例。這種思想給了開發(fā)人員一個錯誤信號,認為自己不應該寫測試,即使寫了測試,也寫不好。
殊不知,由開發(fā)人員編寫測試帶來的收益,最重要的一點不在于測試本身,而在于它能促進開發(fā)、測試以及需求分析人員的交流與溝通。而測試先行的方式也能讓開發(fā)者跳出實現(xiàn)的窠臼,而從業(yè)務角度去看待問題,從消費者角度去思考接口的設計。
倘若開發(fā)者總是憊懶地將測試職責委派給專門的測試人員,漸漸地,就會滋生一種依賴心理。測試人員的精確測試當然可以保障質量,但這種測試通常是黑盒測試,這里保障的質量主要還是外部質量。而且,這種測試帶來的反饋總是慢于開發(fā)進度,一旦發(fā)現(xiàn)缺陷,修復缺陷的成本也會變得更高。
軟件質量除了外部質量之外,內部質量同等重要。
軟件成本等于開發(fā)成本與維護成本之和,而維護成本的增加主要歸咎于內部質量的糟糕。
當我們讓開發(fā)人員為原有代碼編寫單元測試時,總是覺得舉步維艱,主因就在于代碼的可測試性不夠好。要測試一個類,竟然連簡單創(chuàng)建它的對象都變成了不可能完成的任務。在為這樣的代碼編寫單元測試時,就好像被落到了蜘蛛網中,被這些網絲牽住,纏住,如何掙扎都無法擺脫;除非,我們能夠快刀斬亂麻。然而,一旦采用這種粗暴的方式,則對于系統(tǒng)而言,就不是維護,而是重寫了。
測試先行的開發(fā)至少在一定程度規(guī)避了這樣的問題。因為開發(fā)人員首先要寫好測試,這就驅使開發(fā)人員必須強制地思考代碼的可測試性。而在足夠多的測試保護下,即使代碼的內部質量欠佳,要進行重構也更為簡單。
然而,這些好處都不是短期能見成效的,且團隊若不能達成共識,只靠一二人堅定地踐行TDD,在測試覆蓋率不夠的情況下,無異于杯水車薪。多數(shù)開發(fā)者在維護別人的丑陋代碼時,可能會罵聲連連,殊不知同時作為罵者自身,其實也在重復被罵者的故事。
2. 需求分析與任務分解
需求分析能力常常是開發(fā)人員的短板。開發(fā)人員養(yǎng)成了一個習慣,看什么事情都會從技術實現(xiàn)的角度去思考。要實現(xiàn)一個網頁,就會想到如何編寫JavaScript來響應用戶的動作,如何編寫CSS,卻很少思考用戶體驗和操作的流程。要完成一個數(shù)據(jù)分析,總會想到數(shù)據(jù)的屬性,轉換和提取數(shù)據(jù)的算法,卻不會想到分析數(shù)據(jù)的價值以及合理的流程。
對于繁瑣的需求描述,我們總是沒有耐心去深入研讀,而是在掌握了大體意思后,就開始匆匆進行開發(fā)與實現(xiàn)。TDD要求我們在編寫測試之前要做好合理的任務分解。若沒有很好地理解需求,任務分解就無法順利進行。
這就帶來了團隊協(xié)作的問題。
若我們能從需求的源頭進行改進,或許TDD會變得更容易。例如,對故事的拆分更合理,遵循User Story的INVEST原則,那么,要實現(xiàn)的Story在測試性、獨立性方面就會有更好的改觀。如果需求分析人員能夠非常明確地編寫出驗收標準(Acceptance Cretiria),任務分解也會變得更加容易。
更進一步,若需求分析人員能夠參考甚至遵循Specification By Example的方式,采用Given-When-Then的模式來描繪各個用例場景;那么,再要進行任務分解,不就變得輕而易舉嗎?所以說,推行TDD之所以非常艱難,或許***的原因是我們僅僅將目光放到了開發(fā)者身上,卻忽略了需求分析人員扮演的關鍵角色。正所謂:“問渠那得清如許,為有源頭活水來。”
我一直強調任務分解是有層次的。分析需求時,不能一個猛子就扎進繁瑣的實現(xiàn)細節(jié)。要從用戶價值出發(fā),先梳理出最外層的需求任務,然后抽絲剝繭,條分縷析地層層遞進,如此方能理清思路,掌控復雜邏輯。基本上,任務分解可以分為三個層次,即業(yè)務價值——>業(yè)務功能——>業(yè)務實現(xiàn)。這個層次是一種“遞歸”的狀態(tài),視需求的復雜度可以不停向下拆分。
任務分解是TDD的核心,是驅動設計和開發(fā)的重要力量,卻被很多人忽略了。不能不說是一種誤解與遺憾。
3. 測試先行的編程習慣
正所謂“江山易改本性難移”,數(shù)年養(yǎng)成的開發(fā)習慣不可能一朝一夕改變。這恰恰成為許多人反對TDD的借口,鑄造了一塊堅硬的用于防守的盾。
然而,以我個人經驗以及我所觀察到的情況來看,這其中固然有習慣的力量作祟,然而主因還是因為對TDD方法的掌握程度以及一些誤解導致。
前面已經述及,任務分解應該是TDD的起點。多數(shù)開發(fā)者未能形成任務分解的習慣。因此在改變?yōu)闇y試先行的時候,錯以為應該一上來就寫測試。因為思路沒有理清,腦子里一片亂麻,再加上本身對TDD不夠熟悉,編寫測試就變得舉步維艱,總覺得束手束腳,就好像被綁了一只手,又好像是在泥沼中掙扎。許多時候,甚至發(fā)揮不出自己哪怕三分的功力。
一貫以來,我們都在強調測試先行。這容易產生一種錯覺,就是認為TDD必須一開始就寫測試,“簡單設計”嘛,于是就沒有了設計。這讓那些習慣于事先設計的開發(fā)者更難以接受。
那么,TDD是否需要事先設計呢?Martin Fowler的文章Is Design Dead其實就是對此問題的正本清源。我個人認為,視場景而定,測試驅動開發(fā)仍可進行事先設計。
設計并不僅包含技術層面的設計如對OO思想乃至設計模式的運用,它本身還包括對需求的分析與建模。若不分析需求就開始編寫測試,就好像沒有搞清楚要去的地方,就開始快步前行,***才發(fā)現(xiàn)南轅北轍一般。
測試驅動開發(fā)提倡的任務分解,實際上就是一種需求的分析。如何尋找職責,以及識別職責的承擔者則可以視為建模設計。
測試驅動像是一種培養(yǎng)設計專注力的手段,就像冥想者通過盤腿靜坐的手段來體悟天地一樣,測試驅動可以強迫你站在測試的角度(就是使用者的角度)去思考接口,如此才能設計出表現(xiàn)意圖的接口。
在開始測試驅動開發(fā)之前,做適度的事先設計,還有利于我們仔細思考技術實現(xiàn)的解決方案。它與測試驅動接口的設計并不相悖。解決方案或許屬于實現(xiàn)層面,若過早思考實現(xiàn),會干擾我們對接口的判斷;但完全不理會實現(xiàn),又可能導致設計方向的走偏。
例如,我們要實現(xiàn)XML消息到Java對象的轉換。
一種解決方案是通過jaxb將消息轉換為Java對象,然后再定義轉換映射的Transformer,通過硬編碼或者反射的方式將其轉換為相關的領域對象;然后在執(zhí)行了業(yè)務操作后,再將返回的結果轉換為另一個Jaxb對象。另一種解決方案則是通過引入模板,例如StringTemplate或者Velocity,定義轉換的模板,然后進行替換實現(xiàn)。這兩種解決方案的區(qū)別,直接影響了我們劃分任務的方式。
所以,在運用TDD時,先不要一巴掌拍死,可以先抱著開放的態(tài)度嘗試嘗試。何況,TDD并非一招鮮,吃遍天,總要有適合它的場景。例如UI的開發(fā),交互協(xié)作的控制邏輯,數(shù)據(jù)庫開發(fā),并發(fā)處理,都不是運用TDD的好場景。
4. 重構能力
TDD的核心是紅——綠——重構。這意味著重構是TDD非常重要的一環(huán),它直接關系到TDD開發(fā)出來的代碼質量。沒有好的重構能力,TDD就會有缺失。若說代碼的內部質量是生命的話,重構就是靈魂,缺少了它,代碼就沒有靈性了。多數(shù)時候實施TDD,都會因為重構能力的缺乏而陷入困境。
重構的關鍵首先在于如何識別代碼的壞味道。這需要代碼閱讀的千錘百煉,而非死記硬背Martin Fowler在《重構》一書中總結的壞味道。當這些壞味道變成你的一種直覺,甚至就像與生俱來的一種能力時,你就會降低對糟糕代碼的容忍度。在你眼中,這些爛代碼就是垃圾,必須清掃,否則無法“安居”。
重構手法與代碼壞味道一一對應。若有測試保障,重構就變得安全。但盡可能地,我們還是希望運用工具提供的自動重構功能,這既提高了重構效率,也在一定程度下確保了重構的安全。
當然,重要的是要找到重構的節(jié)奏感,即小步前行,每次重構必運行測試的良好習慣。若能結合分布式版本管理系統(tǒng)如Git,做到原子提交,就會更加方便。即使重構出現(xiàn)問題,也可以快速地回到前面的版本快照。
在TDD過程中,若能結對自然是上佳選擇。當一個人在掌控鍵盤時,另一個人就可以重點關注代碼的可讀性,看看代碼是否散發(fā)出臭味。兩個人的眼睛終歸要更銳利一些,至少視野的范圍更廣泛。
及時重構是重構諸多實踐中最重要的一點。不要讓重構成為你在未來償還債務的殺手锏。越拖到后面,償還債務的成本就越高。以重構而論,如果將重構拖到***,則需要的重構能力就更強,因為程序結構會變得更復雜。當然,只要你的代碼能夠保證足夠的覆蓋率,以及較好的松散耦合,重構依舊可行。采用TDD,基本能滿足這兩條要求。但以成本而論,小步前行才是重構之道。
5. 單元測試的基礎設施
***說說單元測試的基本設施。很多時候,這可能不是問題;但很多時候,這可能會成為大問題。面對諸如測試數(shù)據(jù)準備等問題,需要認真分析,找到應對方案。
原則上,***能找到一些開源的測試框架,包括生成測試數(shù)據(jù),模擬測試行為等。因為你遇到的問題,別人可能早已遇見過。這個世界上有很多聰明而又樂于分享的程序員,不要局限在自己公司一隅。睜大眼睛看看滿世界吧。所謂“君子生非異也,善假于物也”。好程序員,也要這樣。
說不定,你會拋棄TDD,因為你找到了更好的適合你的做法。
【本文為51CTO專欄作者“張逸”原創(chuàng)稿件,轉載請聯(lián)系原作者】