本文僅就單元測試而論,雖然是說的測試,但目的是驅(qū)動開發(fā),不過也不是談測試驅(qū)動開發(fā),更象是對測試驅(qū)動開發(fā)時TEST FIRST這個過程中如何保證測試代碼的正確性的理解和想法,當然有一些,我認為是通用的,不管是不是測試優(yōu)先。而我目前接觸最多的還是JAVA的單元測試,所以談的東西還是以JAVA為主,舉的例子都是和JAVA有關的。
另外前些天看到一個帖子有人問這樣的問題,想到當初自己剛接觸JUNIT單元測試時也有類似的困惑,現(xiàn)在有了一些經(jīng)驗,所以寫下來,既是對自己經(jīng)驗的總結,也是希望能有人相互討論提高。
首先是我認為要做到測試代碼的正確性的幾個要點:
一、TEST FIRST
二、只寫出需求測試(注意,是目標代碼需求,這個代碼的用戶當然是自己了,^_^)
三、不要為了測試而測試(這句話是和一個朋友聊天時他說是他和Kent Back交流時Kent Back提醒他的,這里我也不是很確定對這句話的理解的正確與否,因為理解一句話,上下文也是關鍵的,而我并不很了解我朋友同Kent Back談話的具體內(nèi)容和過程,不過這里還是作為一個要點談談自己的想法)
四、每次寫一點(原子級)測試
五、Clean code that works,(當然包括測試代碼啦,^_^)
六、對于一個應用框架,是針對這個框架先寫一個測試框架(這其實是一個很具體的內(nèi)容,不過現(xiàn)在JAVA在WEB方面用得很多,測試相對來說也比較難些,所以有這點)
七、時刻提醒自己TEST FIRST的目的。(我們的目的是驅(qū)動開發(fā),而不是為了測試,呵呵,這點是前面第一點和第二點和起來一樣,之所以還要單獨列,只是再次提醒,所以這一點我后面不作詳細闡述)
八、偷懶是程序員的通病,但是小偷懶就別了。(我有這樣的觀點:程序員的水平高低,其實從他偷懶的程度上是可以看出來的……^_^)
在開始具體來說上述要點之前,我想先寫個例子,只是覺得應該寫個例子,^_^,我的文采實在不是很好的,所以憑感覺的。
一個例子:
我有一個專門用于將數(shù)據(jù)庫操作結果集(ResultSet)解析成一個DOM的Document對象的類,這個類可以根據(jù)給定一個XML配置模板中的一個定義節(jié)點(declare節(jié)點)的子節(jié)點(column節(jié)點)集合來解析ResultSet生成Document對象,其中每個column節(jié)點都定義了要從ResultSet中獲取的某一個字段的屬性,包括字段名、該字段在展示是是否可修改(editable)、是否有格式化模式屬性(pattern)用于格式化該字段的數(shù)據(jù)等等;為了做得更通用,也可以依據(jù)ResultSet返回的ResultSetMetaData對象來生成column節(jié)點,再由column節(jié)點獲取數(shù)據(jù)體,當然這樣做有很大的局限性,比如該column節(jié)點的pattern、editable等等屬性的設置都不會太靈活。這個設計,其的靈活性被放在XML配置模板上,由模板的定義獲取數(shù)據(jù),并且定義了數(shù)據(jù)的展示屬性,而一旦column節(jié)點是根據(jù)給定的ResultSet來自動生成時,靈活性大大折扣,雖然在大多數(shù)應用中,都不會使用由ResultSet來自動生成,但是如果一開始并不能確定定義列時卻是必須這樣做,特別是在ResultSet輸出的字段數(shù)量是變化的時候。問題終于出來了,最近有一個應用就是這樣,首先是必須要使用從ResultSet獲取定義節(jié)點(column),然后,在完成了所有的代碼后,發(fā)現(xiàn)給定ResultSet中都存在冗余字段,這個時候,沒辦法,只能是修改程序來適應它了。
在遇到這個麻煩,并確定必須修改自己代碼后(老實說,第一反應當然是讓人修改SQL來去掉冗余字段了,因為最初的設計根本就是不能有冗余數(shù)據(jù)的,不過確實是大家都有本難念的經(jīng)啊,SQL是不能改了,因為數(shù)據(jù)庫端的實現(xiàn)使用了一個穩(wěn)定的公共實現(xiàn),慶幸的是冗余字段是相同的),我腦袋里蹦出的第一個念頭是添加一個事件監(jiān)聽器,來監(jiān)聽這個應用中生成數(shù)據(jù)部分(為了敘述方便,姑且叫“dataBuilder”吧)的代碼,一旦數(shù)據(jù)生成就觸發(fā)ResultSet解析完成事件,然后我可以寫監(jiān)聽器來處理解析完成的結果集(當然就是將冗余的數(shù)據(jù)CUT掉啦),這樣以后再出現(xiàn)其它類似的狀況,我可以通過添加新的監(jiān)聽器來過濾數(shù)據(jù)而不需要動原有的代碼。SWEAT,看來這個想法還算可行,至少比寫一個子類看起來簡單多,以后修改也容易多,動手吧。(其實要注意這個事情的發(fā)生環(huán)境,首先是碼本來都OK了的,而后來突然發(fā)現(xiàn)這個問題,而這個問題是需要立刻修改掉的,所以沒有太多時間來仔細考慮,我總是犯這樣的錯誤。)
這個時候我有兩個選擇,一是直接就寫代碼,一是先寫個測試。當然,我選擇的是先寫個測試,之所以擺明了這二個當然是為了比較了。先說如果我先寫代碼,那么我就直接進入了目標功能的角色,因為現(xiàn)在似乎目標很明確,我要給dataBuilder添加一個能處理監(jiān)聽器的功能,在使用ResultSet解析器解析完成一個Document后觸發(fā)解析完成事件,通知所有注冊的監(jiān)聽器,并將解析完成的結果通過事件對象傳遞給監(jiān)聽器進行處理。在以前,我會立刻想到如何添加事件,如何處理事件列表,還有兩個必要的接口,一個是監(jiān)聽器接口,一個是事件接口,一些簡要的構思之后,不用多少時間就可以完成這些工作,然后就開始調(diào)試。
上面那么說,主要是為了對比,現(xiàn)在我是先寫測試的。當然,如上所述目標現(xiàn)在似乎也很明確的,那么無論如何寫個測試類吧,在寫上必要的to do list之后,我開始想如果現(xiàn)在代碼寫完了,我要怎樣來使用它呢。哦,對了,測試這個之前還要寫個測試使用的監(jiān)聽器實現(xiàn),這個實現(xiàn)可以很簡單,把解析結果Document干掉好了,呵呵,驗證結果還更容易,那就把它所有節(jié)點remove掉好了,^_^。(注:這里的測試還沒有到主功能,只是先做測試監(jiān)聽器部分)不過這個時候,我突然覺得目前這個設計似乎還是有些麻煩(懶惰是程序員的通病,sweat,每次想偷懶都想起這句),要寫監(jiān)聽器,還要讓dataBuilder處理監(jiān)聽列表觸發(fā)事件,雖然讓過濾數(shù)據(jù)操作可以很獨立地添加,而監(jiān)聽器的注冊也可以通過XML配置文件來完成,但還是顯得多余,好像目前這個需求只要一個Decorator模式,寫一個修飾類來修飾ResultSet解析器就差不多了,以后需要新的功能,換Decorator就可以了,也可以相互嵌套來完成多個功能,這樣的話,就不需要對dataBuilder動太多手腳了。sweat,還好自己沒動手瞎忙(無論如何,測試優(yōu)先讓我重新認識自己的設計,以及目標,于是我義無反顧地拋棄了原來的想法)。新目標出現(xiàn)了,看來我需要一個解析器接口實現(xiàn)的Decorator類(注:這個ResultSet解析器類原本就是一個接口ResultSetParser的實現(xiàn)),我可以先寫一個擴展解析器接口的抽象類來包裝下,以后的Decorator實現(xiàn)都從這個抽象類繼承,直接實現(xiàn)修飾內(nèi)容就可以了(實際我一直認為,在Decorator模式中所有Decorator實現(xiàn)類都有一個父抽象類繼承自修飾目標類的接口,其最主要的目的是使Decorator實現(xiàn)類功能更清晰,因為實際這個抽象類要包裝的東西其實很少,這些移到子類中也完全可以,這樣的話子類就是直接實現(xiàn)修飾目標類的接口了,效果一樣,所以我認為這里有一個這樣的抽象類統(tǒng)一由所有修飾類繼承,更主要的是宣布,一個修飾目標類接口的實現(xiàn)類它是一個修飾類,這在閱讀程序,以及使用該API上可以達到很好的效果。)。OK,就這么辦(我總是很容易下決定呢,^_^)。
另外前些天看到一個帖子有人問這樣的問題,想到當初自己剛接觸JUNIT單元測試時也有類似的困惑,現(xiàn)在有了一些經(jīng)驗,所以寫下來,既是對自己經(jīng)驗的總結,也是希望能有人相互討論提高。
首先是我認為要做到測試代碼的正確性的幾個要點:
一、TEST FIRST
二、只寫出需求測試(注意,是目標代碼需求,這個代碼的用戶當然是自己了,^_^)
三、不要為了測試而測試(這句話是和一個朋友聊天時他說是他和Kent Back交流時Kent Back提醒他的,這里我也不是很確定對這句話的理解的正確與否,因為理解一句話,上下文也是關鍵的,而我并不很了解我朋友同Kent Back談話的具體內(nèi)容和過程,不過這里還是作為一個要點談談自己的想法)
四、每次寫一點(原子級)測試
五、Clean code that works,(當然包括測試代碼啦,^_^)
六、對于一個應用框架,是針對這個框架先寫一個測試框架(這其實是一個很具體的內(nèi)容,不過現(xiàn)在JAVA在WEB方面用得很多,測試相對來說也比較難些,所以有這點)
七、時刻提醒自己TEST FIRST的目的。(我們的目的是驅(qū)動開發(fā),而不是為了測試,呵呵,這點是前面第一點和第二點和起來一樣,之所以還要單獨列,只是再次提醒,所以這一點我后面不作詳細闡述)
八、偷懶是程序員的通病,但是小偷懶就別了。(我有這樣的觀點:程序員的水平高低,其實從他偷懶的程度上是可以看出來的……^_^)
在開始具體來說上述要點之前,我想先寫個例子,只是覺得應該寫個例子,^_^,我的文采實在不是很好的,所以憑感覺的。
一個例子:
我有一個專門用于將數(shù)據(jù)庫操作結果集(ResultSet)解析成一個DOM的Document對象的類,這個類可以根據(jù)給定一個XML配置模板中的一個定義節(jié)點(declare節(jié)點)的子節(jié)點(column節(jié)點)集合來解析ResultSet生成Document對象,其中每個column節(jié)點都定義了要從ResultSet中獲取的某一個字段的屬性,包括字段名、該字段在展示是是否可修改(editable)、是否有格式化模式屬性(pattern)用于格式化該字段的數(shù)據(jù)等等;為了做得更通用,也可以依據(jù)ResultSet返回的ResultSetMetaData對象來生成column節(jié)點,再由column節(jié)點獲取數(shù)據(jù)體,當然這樣做有很大的局限性,比如該column節(jié)點的pattern、editable等等屬性的設置都不會太靈活。這個設計,其的靈活性被放在XML配置模板上,由模板的定義獲取數(shù)據(jù),并且定義了數(shù)據(jù)的展示屬性,而一旦column節(jié)點是根據(jù)給定的ResultSet來自動生成時,靈活性大大折扣,雖然在大多數(shù)應用中,都不會使用由ResultSet來自動生成,但是如果一開始并不能確定定義列時卻是必須這樣做,特別是在ResultSet輸出的字段數(shù)量是變化的時候。問題終于出來了,最近有一個應用就是這樣,首先是必須要使用從ResultSet獲取定義節(jié)點(column),然后,在完成了所有的代碼后,發(fā)現(xiàn)給定ResultSet中都存在冗余字段,這個時候,沒辦法,只能是修改程序來適應它了。
在遇到這個麻煩,并確定必須修改自己代碼后(老實說,第一反應當然是讓人修改SQL來去掉冗余字段了,因為最初的設計根本就是不能有冗余數(shù)據(jù)的,不過確實是大家都有本難念的經(jīng)啊,SQL是不能改了,因為數(shù)據(jù)庫端的實現(xiàn)使用了一個穩(wěn)定的公共實現(xiàn),慶幸的是冗余字段是相同的),我腦袋里蹦出的第一個念頭是添加一個事件監(jiān)聽器,來監(jiān)聽這個應用中生成數(shù)據(jù)部分(為了敘述方便,姑且叫“dataBuilder”吧)的代碼,一旦數(shù)據(jù)生成就觸發(fā)ResultSet解析完成事件,然后我可以寫監(jiān)聽器來處理解析完成的結果集(當然就是將冗余的數(shù)據(jù)CUT掉啦),這樣以后再出現(xiàn)其它類似的狀況,我可以通過添加新的監(jiān)聽器來過濾數(shù)據(jù)而不需要動原有的代碼。SWEAT,看來這個想法還算可行,至少比寫一個子類看起來簡單多,以后修改也容易多,動手吧。(其實要注意這個事情的發(fā)生環(huán)境,首先是碼本來都OK了的,而后來突然發(fā)現(xiàn)這個問題,而這個問題是需要立刻修改掉的,所以沒有太多時間來仔細考慮,我總是犯這樣的錯誤。)
這個時候我有兩個選擇,一是直接就寫代碼,一是先寫個測試。當然,我選擇的是先寫個測試,之所以擺明了這二個當然是為了比較了。先說如果我先寫代碼,那么我就直接進入了目標功能的角色,因為現(xiàn)在似乎目標很明確,我要給dataBuilder添加一個能處理監(jiān)聽器的功能,在使用ResultSet解析器解析完成一個Document后觸發(fā)解析完成事件,通知所有注冊的監(jiān)聽器,并將解析完成的結果通過事件對象傳遞給監(jiān)聽器進行處理。在以前,我會立刻想到如何添加事件,如何處理事件列表,還有兩個必要的接口,一個是監(jiān)聽器接口,一個是事件接口,一些簡要的構思之后,不用多少時間就可以完成這些工作,然后就開始調(diào)試。
上面那么說,主要是為了對比,現(xiàn)在我是先寫測試的。當然,如上所述目標現(xiàn)在似乎也很明確的,那么無論如何寫個測試類吧,在寫上必要的to do list之后,我開始想如果現(xiàn)在代碼寫完了,我要怎樣來使用它呢。哦,對了,測試這個之前還要寫個測試使用的監(jiān)聽器實現(xiàn),這個實現(xiàn)可以很簡單,把解析結果Document干掉好了,呵呵,驗證結果還更容易,那就把它所有節(jié)點remove掉好了,^_^。(注:這里的測試還沒有到主功能,只是先做測試監(jiān)聽器部分)不過這個時候,我突然覺得目前這個設計似乎還是有些麻煩(懶惰是程序員的通病,sweat,每次想偷懶都想起這句),要寫監(jiān)聽器,還要讓dataBuilder處理監(jiān)聽列表觸發(fā)事件,雖然讓過濾數(shù)據(jù)操作可以很獨立地添加,而監(jiān)聽器的注冊也可以通過XML配置文件來完成,但還是顯得多余,好像目前這個需求只要一個Decorator模式,寫一個修飾類來修飾ResultSet解析器就差不多了,以后需要新的功能,換Decorator就可以了,也可以相互嵌套來完成多個功能,這樣的話,就不需要對dataBuilder動太多手腳了。sweat,還好自己沒動手瞎忙(無論如何,測試優(yōu)先讓我重新認識自己的設計,以及目標,于是我義無反顧地拋棄了原來的想法)。新目標出現(xiàn)了,看來我需要一個解析器接口實現(xiàn)的Decorator類(注:這個ResultSet解析器類原本就是一個接口ResultSetParser的實現(xiàn)),我可以先寫一個擴展解析器接口的抽象類來包裝下,以后的Decorator實現(xiàn)都從這個抽象類繼承,直接實現(xiàn)修飾內(nèi)容就可以了(實際我一直認為,在Decorator模式中所有Decorator實現(xiàn)類都有一個父抽象類繼承自修飾目標類的接口,其最主要的目的是使Decorator實現(xiàn)類功能更清晰,因為實際這個抽象類要包裝的東西其實很少,這些移到子類中也完全可以,這樣的話子類就是直接實現(xiàn)修飾目標類的接口了,效果一樣,所以我認為這里有一個這樣的抽象類統(tǒng)一由所有修飾類繼承,更主要的是宣布,一個修飾目標類接口的實現(xiàn)類它是一個修飾類,這在閱讀程序,以及使用該API上可以達到很好的效果。)。OK,就這么辦(我總是很容易下決定呢,^_^)。