Practicing Test-Driven Development by Example Using Delphi
零雨其蒙原創 轉載請注明
1 測試驅動開發
測試驅動開發不是什么噱頭,而是真正有用的開發實踐。今天派給我一個任務,讓我解決一下退休提醒功能的Bug,我沒看出原來的代碼有何錯誤,不過覺得設計思路不十分的好:將數據庫中所有的員工都取出來,然后再篩選應該被提醒的員工。而我覺得應該直接到數據庫中去做篩選,返回大量無用的數據是資源的巨大浪費(我們在甲方公司里面開發,其網絡之差令人發指)。
正好,我在研究TDD(本文有時指的是測試驅動開發,有時指的是測試驅動設計,因為兩者都是存在的),想想何不來一次徹底的實踐,真正的、完整的來一次TDD,體味一下其中的樂趣。
由于我們的開發工具是Delphi,因此自動測試工具自然而然就要使用DUnit了。盡管有的人說TDD不一定非得用自動測試框架,我也在使用VB進行OO系統開發時,用自制的測試程序進行測試,不過覺得那樣都有一種不爽的感覺。因為總需要去維護復雜的測試代碼,不能全力投入到測試驅動設計中。
2 準備工作
首先介紹一下業務:
很簡單,就是在一個界面上顯示即將退休的人員,具體提前多少天顯示是從數據庫中讀取的參數。
然后配置DUnit環境,網上有n多教程,然后安裝了一個DUnit plug-in插件,方便開發,網上也有講解。
將Stop on Delphi Exception前的對號取消,這樣就不會在出現異常時跳出了。
3 開始TDD之旅
本文是我進行TDD的實踐記錄,當然其間的思考要比這個多一些,不過主體部分基本都包含了,而且絕對寫實。本文不是TDD的頌歌,我也提出了自己在實踐中遇到的困難和疑惑。希望能給讀者帶來啟示。
創建工程文件HR.dpr,然后使用DUnit plug-in,New Project,就自動在HR.dpr所在文件夾建了一個dunit文件夾,新建的測試工程默認名為HRTests,這是很好的規范,默認即可。然后New TestModule,建立一個測試單元。
接下來的工作就是在這兩個同時開著的工程中開始工作了,一會我會切換到HRTests編寫測試用例,一會我會在HR下編寫產品代碼,然后再回到HRTests下運行Dunit,進行測試。
3.1 領域驅動設計
首先構建領域層,領域概念就是退休(Retired),退休人員(EmployeeRetired)了。
先創建這兩個類,不少文章說先建立測試用例,然后測試時肯定顯示紅條,因為被測試的類還沒有建立,我覺得沒建立的話連編譯都過不了,怎么運行DUnit啊?
然后就可以開始根據想象編寫測試用例了。思考對象的責任和工作方式,然后切換到產品工程添加這些責任。(有點像一邊畫順序圖一邊畫類圖進行責任分配)
首先,我創建類TRetire
給TRetire類分配一個責任:查找退休提醒參數:
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
function TRetire.getretireAwokeParaList: TObjectList;
var paraList:TObjectList;
begin
end;
這是一個空殼,沒有實際的內容,然后切換到HRTest工程,會出現下面的對話框。

在HR工程做了任何改動,保存后,都會在HRTest中有提醒。
可能很多人從來沒見過測試用例長什么樣子,下面就給出一個完整的例子。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
unit HRTestsTests;
interface
uses
TestFrameWork,
URetire,
Contnrs;
type
TTestRetire=class(TTestCase)
private
retire:TRetire;
retirePara:TRetirePara;
protected
procedure SetUp; override;
procedure TearDown; override;
published
procedure testGetretireAwokeParaList;
end;
implementation
function UnitTests: ITestSuite;
var
ATestSuite: TTestSuite;
begin
ATestSuite := TTestSuite.Create('Retire tests');
ATestSuite.AddTests(TTestRetire);
Result := ATestSuite;
end;
{ TTestRetire }
procedure TTestRetire.SetUp;
begin
inherited;
retire:=TRetire.Create;
retirePara:=TRetirePara.Create;
end;
procedure TTestRetire.TearDown;
begin
inherited;
retire.Free;
retirePara.Free;
end;
procedure TTestRetire.testGetretireAwokeParaList;
var paraList:TObjectList;
begin
paraList:=retire.getretireAwokeParaList;
retirePara:=TRetirePara(paraList.Items[0]);
check(retirePara._emp_type='管理人員');
check(retirePara._sex='男');
check(retirePara._retireage='60');
check(retirePara._uptime='90');
retirePara:=TRetirePara(paraList.Items[1]);
check(retirePara._emp_type='管理人員');
check(retirePara._sex='女');
check(retirePara._retireage='55');
check(retirePara._uptime='90');
retirePara:=TRetirePara(paraList.Items[2]);
check(retirePara._emp_type='工人');
check(retirePara._sex='男');
check(retirePara._retireage='60');
check(retirePara._uptime='90');
retirePara:=TRetirePara(paraList.Items[3]);
check(retirePara._emp_type='工人');
check(retirePara._sex='女');
check(retirePara._retireage='50');
check(retirePara._uptime='90');
end;
initialization
RegisterTest('Retire test',UnitTests);
end.
在編寫測試用例時,我發現返回的不是一個一維的表,而是二維的,我還是使用了對象來報存另一維的數據,又創建了一個名為TRetirePara的類。其屬性如上面的代碼所示。

然后編譯運行HRTest,出現DUnit,點擊綠色的RUN按鈕,出現紅條。錯誤是EAccessViolation。
這是因為沒有創建TObjectlist的實例paraList,而在testGetretireAwokeParaList中訪問了它,這時切換到HR工程,在getretireAwokeParaList中添加如下代碼。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
function TRetire.getretireAwokeParaList: TObjectList;
var paraList:TObjectList;
begin
paraList:=TObjectList.Create;
getretireAwokeParaList:=paraList;
end;
再次測試。

依然顯示紅色,錯誤是List index out of bounds,這是因為在getretireAwokeParaList方法中并沒有向paraList中添加任何對象,這時需要再對getretireAwokeParaList進行重構。
3.2 步伐到底要多小?
當有了“測試驅動依賴癥”后,就想讓DUnit幫我思考一些內容,比如下一步應該編寫什么。通過測試,我不斷的清楚了自己下一步的任務。但是或許不需要如此小步的前進,如果已經有了很好的思路,可以一下子把剛才的程序都編完,甚至把整個getretireAwokeParaList方法都完成。
然而由于沒有太多的plan,想一點編一點的,難免就會采取這樣小的步伐,其實這樣也很好,因為不至于寫了一大堆,錯了都不知道哪一句是罪魁禍首。經常看到有人在面對一堆不知道在哪個地方出錯的代碼時,采用了刪除所有,然后一句一句還原,發現到哪句錯誤就改哪句。這種做法一般都被用于沒有調試器的環境,比如HTML頁面。如果有了調試器,傳統的做法當然是設置斷點,然后利用調試器進行跟蹤。關掉調試器,以測試代替調試的一個支撐點是,不大可能會出現大段的需要你去跟蹤錯誤的代碼,因為很小的一步重構進行之后,就開始測試了,哪里有錯誤一目了然。當然調試器的作用并不只是跟蹤某個變量在運行時的值的變化,還有理解代碼在匯編一級上是如何工作的,這將更加有利于你調錯。但是,總而言之,調試器肯定是幫助你調試錯誤的,小步前進的單元測試可以幫助你在不使用調試器的情況下,找出錯誤。
3.3 混入持久層的測試
或許看到這篇文章的您,有更好的方法來完成這樣的測試,請您告訴我,因為我也是使用Dunit進行TDD的新手,希望分享您的寶貴經驗。
我們使用的是ODAC控件連接ORACLE數據庫。在產品代碼(HR.dpr)中,通常我們都是直接加載數據模塊中TOraSession,來連接數據庫,再用TOraQuery與之相連。getretireAwokeParaList的主要操作是從數據庫中獲取記錄,產品代碼如下:
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
function TRetire.getretireAwokeParaList: TObjectList;
var paraList:TObjectList;
retirePara:TRetirePara;
begin
paraList:=TObjectList.Create;
_qry.Close;
_qry.SQL.Clear;
_qry.SQL.Text:='select * from HR1_RETIREPARAMETER';
_qry.Open;
if _qry.RecordCount>0 then
begin
_qry.First;
while not _qry.Eof do
begin
retirePara:=TRetirePara.Create;
retirePara._emp_type:=_qry.FieldByName('EMPLOYEE_TYPE').AsString;
retirePara._sex:=_qry.FieldByName('SEX').AsString;
retirePara._retireage:=_qry.FieldByName('RETIRE_AGE').AsString;
retirePara._uptime:=_qry.FieldByName('UPTIME_DAYS').AsString;
paraList.Add(retirePara) ;
_qry.Next;
end;
end;
getretireAwokeParaList:=paraList;
end;
這比你在上一節看到的代碼又豐富了許多。_qry是用到的TOraQuery類型的變量,它接受TOraQuery實例。由于進行單元測試時,HR.dpr是不啟動的,因此DM根本就不會被創建。
考慮再三,我在測試項目HRTest.dpr中加入了數據庫連接代碼,并將創建的TOraQuery實例賦值給TRetire的屬性(property)qry。代碼如下:
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
procedure TTestRetire.SetUp;
begin
inherited;
_qry:=TOraQuery.Create(nil);
{ _session:=TOraSession.Create(nil);
_session.Server:=';
_session.ConnectString:='';
_session.Username:='';
_session.Password:='';
_session.ConnectPrompt:=false;
_session.Connect; }
_dmhr:=TDMHR.Create(nil);
_session:=TOraSession.Create(nil);
_session:= _dmhr.HRSession ;
_qry.Session:=_session;
retire:=TRetire.Create;
retire.qry:=_qry;
end;
起初,我創建了TOraSession對象,可是不知道為什么說驅動器有錯誤,于是就創建了一個DM(數據模塊),然后獲得其中的HRSession。注釋掉的代碼有何錯誤還請高人指點。之后我又寫了個測試連接是否成功的方法。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
procedure TTestRetire.testConnect;
begin
check(_session.Connected=true);
end;
之后再進行測試,綠條終于出現了!

我在之前還犯了錯誤,總是出現紅條。后來才發現原來對象創建沒搞清楚。這個錯誤在我編寫Java程序時也犯過,看來一定要注意阿。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
while not _qry.Eof do
begin
retirePara:=TRetirePara.Create;
retirePara._emp_type:=_qry.FieldByName('EMPLOYEE_TYPE').AsString;
//省略若干行
paraList.Add(retirePara) ;
_qry.Next;
end;
end;
原來將retirePara:=TRetirePara.Create;這句寫在循環之外了,結果測試時,只有最后一條(paraList.Item[3]對應的結果)是正確的。這個錯誤大家一看就知道了,每循環一次都需要創建一個retirePara對象,要不然向paraList添加的其實都是一個對象,而paraList的每個元素都是指向同一個對象引用,賦值之后,當然每個對象的屬性值都是一樣的啦。
還有,在測試用例中進行比較時,我剛開始圖省事,都使用check,結果錯了后沒有任何提示,后來到TestFramework中查到了CheckEquals方法,這個方法很好,如果出錯了,會告訴你期望值是什么,實際值是什么。
3.4 進化式設計
TDD韻律操是:編寫單元測試——〉測試,紅條——〉編寫產品代碼——〉綠條——〉編寫單元測試——〉測試,紅條——〉編寫產品代碼——〉綠條,編寫產品代碼并不能總是一氣呵成,因此就會在編寫部分產品代碼——〉綠條——〉重構——〉測試,綠條/紅條——〉重構——〉測試,綠條/紅條
下面開始編寫function retireAwoke(qry:TOraQuery):TObjectList;方法。還是先寫測試用例。
這里面有個問題,這個方法的作用是查詢并返回所有的符合退休條件的員工,每天的人可能都不一樣,那么該怎樣寫測試用例呢?這個或許應該從DUnit的測試裝備中讀取(如果DUnit有的話,我也不知道有沒有),也可以從一個文本文件或者Excel中讀取,這樣或許好些。但是這依然不是一個可回歸測試。
最終想的辦法是通過SQL語句先查詢一下,看看有哪些記錄,而且這個SQL語句與程序中的不盡相同。主要是將計算退休者生日的程序寫在了SQL中還是寫在程序中的區別。
使用如下的SQL語句進行查詢:
select employeeid,employeename,dptname,birthday,sex,EMPLOYEETYPE
from HR1_EMPLOYEE left join hr1_workdept on hr1_employee.workdeptid=hr1_workdept.dptid
where sex='男' and EMPLOYEETYPE='管理人員'
and BIRTHDAY between '1947-05-24' and (select to_char(to_date('1947-05-24','yyyy-mm-dd') + interval '90' day,'yyyy-mm-dd')
from dual) order by dptid asc;
用于返回60歲退休的男性管理人員(提前90天提醒)。以下是SQL Plus的查詢結果。
EMPLOYEEID EMPLOYEENA DPTNAME BIRTHDAY SE EMPLOYEETY
---------- ---------- ------------------- ---------- -- ----------
YG000043 張三豐 人力資源部 1947-04-16 男 管理人員
已選擇 1 行
還需要說明的是,有四種情況需要測試,但是為了快速實現,我只是寫了其中一種情況,即男,管理人員。然后我就寫了測試程序。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
procedure TTestRetire.testRetireAwoke;
var employeeRetiredList:TObjectList;
i:integer;
begin
employeeRetiredList:=TObjectList.Create;
_employeeRetired:=TEmployeeRetired(employeeRetiredList.Items[0]);
CheckEquals(' YG000043',_employeeRetired._ID);
CheckEquals('張三豐',_employeeRetired._Name);
end;
產品代碼只是讀出了參數列表的第一種情況。然后嵌套進SQL語句中進行查詢。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
function TRetire.retireAwoke(qry:TOraQuery): TObjectList;
var employeeRetired:TEmployeeRetired;
paraList,employeesRetired:TObjectList;
retirePara:TRetirePara;
strSQL,strBirthdayUp, strBirthdayDown:string;
begin
_qry:= qry;
employeesRetired:=TObjectList.Create;
paraList:=getretireAwokeParaList;
retirePara:=TRetirePara(paraList.Items[0]);
//滿足條件的男管理人員
dtBirthday:=EncodeDate(Yearof(date)-StrToInt(retirePara._retireage),monthof(date),DayOf(date));
strBirthdayDown:=formatdatetime('yyyy-mm-dd',dtBirthday);
strBirthdayUp:=formatdatetime('yyyy-mm-dd',dtBirthday+strtoint(retirePara._uptime));
_qry.Close;
_qry.SQL.Clear;
strSQL:='select employeeid,employeename,dptname,birthday,sex,EMPLOYEETYPE';
strSQL:=strSQL +' from HR1_EMPLOYEE left join hr1_workdept on hr1_employee.workdeptid=hr1_workdept.dptid ';
strSQL:=strSQL +' where sex=:sex';
strSQL:=strSQL +' and EMPLOYEETYPE=:EMPLOYEETYPE and BIRTHDAY between '+''''+strBirthdayDown+''''+' and '+''''+strBirthdayUp+''''; strSQL:=strSQL+'order by dptid asc';
_qry.SQL.Text :=strSQL;
_qry.ParamByName('EMPLOYEETYPE').AsString :=retirePara._emp_type;
_qry.ParamByName('SEX').AsString :=retirePara._sex;
_qry.Open;
if _qry.RecordCount>0 then
begin
while not _qry.Eof do
begin
employeeRetired:=TEmployeeRetired.Create;
employeeRetired._ID:=_qry.FieldByName('employeeid').AsString;
//以下省略若干代碼
employeesRetired.Add(employeeRetired);
end;
end;
retireAwoke:=employeesRetired;
end;
很高興,測試通過了!不過實話實說,也并非一次就能做成功的,其中SQL語句寫錯了,就查了半天,DUnit報錯說missing expression。我就是一個這樣馬虎的人,很難把程序一下子寫對,有DUnit做保證,小步前進,以免陷入絕境,在Bug叢生的密林中尋找,當是非常痛苦的事情了。
3.5 重構
對于XP,我覺得簡單設計、TDD、重構、持續集成、小規模發布是連在一起的實踐。簡單設計而沒有重構,就變成了Code and Fix。只有重構,而沒有單元測試的保證,無異于徒手穿越原始森林,沒有安全保證。單元測試,繼而持續集成、小規模發布,才能實現工作的軟件。再加之結對編程,以提高代碼質量;完全客戶現場,以捕獲最真實的需求和得到最真實的反饋,以最快速最真實的態度響應變化;集體代碼所有制,以減少人員流動的風險,和提高復用;40小時工作日,以避免累死和創建更優質的代碼。這是我體會到的XP實踐的好處,隨著實踐和思考的增多,我覺得自己越來越認同XP的觀點了。
閑言少敘,繼續編程。繼續完成其余三種情況,目前而言,就只有四種類型的員工。
| 管理人員 | 工人
男| |
女| |
從注釋(//滿足條件的男管理人員)開始,下面每一類型都要重復一次代碼,當然,可以直接在其中寫循環語句,但是看到Too Long Method恐懼癥,大量的查詢代碼混在這個方法,讓我覺得很別扭,于是我覺得將它們重構出來,采用Extract Method。
首先新建getretireAwokeList方法,將從數據庫中取出需要被提醒的退休人員的代碼Extract到其中。然后整理局部變量。
不斷的編譯,來幫助我檢查錯誤,是不是某些局部變量沒有遷移過來,有沒有變量重名等。經過一番折騰,編譯通過,運行測試。
進行測試,悲劇發生了,在顯示綠條后死機了,我想可能是內存釋放有問題了。
結果果然如此,下面這段程序結束后沒有釋放employeeRetiredList。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
procedure TTestRetire.testRetireAwoke;
var employeeRetiredList:TObjectList;
i:integer;
begin
employeeRetiredList:=TObjectList.Create;
_employeeRetired:=TEmployeeRetired(employeeRetiredList.Items[0]);
CheckEquals(' YG000046',_employeeRetired._ID);
CheckEquals('李隆基',_employeeRetired._Name);
employeeRetiredList.Free;
end;
釋放了employeeRetiredList之后,其中的所有對象也就跟著被釋放了。
3.6 整合UI
最后一步,設計一個展示被提醒的退休人員信息的Form,然后放置一個叫做sgdRetire的StringGrid,這時就是通過列表循環賦值到StringGird中就可以了。
{ 作者:零雨其蒙
創建時間:2007-5-24
Blog:blog.csdn.net/sslaowan
m.tkk7.com/sslaowan
}
procedure TForm1.showEmployeeRetired;
var employeeRetiredList:TObjectList;
i:integer;
_employeeRetired:TEmployeeRetired;
retire:TRetire;
begin
sgdRetire.Cells[0,0]:='員工編號';
sgdRetire.Cells[1,0]:='姓名';
sgdRetire.Cells[2,0]:='部門';
sgdRetire.Cells[3,0]:='生日';
sgdRetire.Cells[4,0]:='性別';
sgdRetire.Cells[5,0]:='工種';
sgdRetire.Cells[6,0]:='距離退休天數';
retire:=TRetire.Create;
employeeRetiredList:=retire.retireAwoke(qryRetire);
sgdRetire.RowCount:= employeeRetiredList.Count+1;
for i:=0 to employeeRetiredList.Count-1 do
begin
_employeeRetired:=TEmployeeRetired(employeeRetiredList.Items[i]);
sgdRetire.Cells[0,i+1]:=_employeeRetired._ID;
sgdRetire.Cells[1,i+1]:=_employeeRetired._Name;
sgdRetire.Cells[2,i+1]:=_employeeRetired._Dept;
sgdRetire.Cells[3,i+1]:=_employeeRetired._Birthday;
sgdRetire.Cells[4,i+1]:=_employeeRetired._Sex;
sgdRetire.Cells[5,i+1]:=_employeeRetired._WorkType;
sgdRetire.Cells[6,i+1]:=_employeeRetired._DaysLeft;
end;
employeeRetiredList.Free;
retire.Free;
end;
但是一定要注意:對象的釋放問題,對象生命周期的開始到完結,一定要好好想清楚。不知道那些癡迷與C/C++的開發者,是如何處理內存釋放問題的,我的Delphi面向對象編程經驗還不算多,對我而言,仔細分析每個對象內存是否被釋放了,真的是一件非常痛苦的事情。所以還是比較喜歡Java那樣帶垃圾回收器的語言。但是通過創建和釋放對象,來理解對象的生命周期意義,對于理解對象還是很有幫助的。
4 真的要選擇TDD嗎?
仔細的設計測試用例,不僅是在思考對象具有哪些責任,同時也是在思考對象如何使用。先總結一下使用TDD的幾點好處:
1. 可以不斷地測試,以保證代碼是正確的。而且在正在開發的這一段時間內,測試是可以不斷進行的,期望的結果不會有什么大的變動。(時間長了就不好說了,比如本文給出的例子。)
2. 由于有了可以重復進行的測試保障,因此可以大膽的進行重構了。
3. 在編寫產品代碼之前考慮什么情況是正確的,而且可以編寫代碼邊增加測試數據,這樣可以有利于全面的測試。據說配合測試裝置,還可以由業務人員填入測試數據,這樣樣本就更大了。
4. 因此結果是騙不了人的,當紅條亮起時,你就知道是剛剛編寫的那段代碼錯了。
5. 在編寫測試用例時,就是在思考這個對象干什么的時候,這有利于養成針對接口編程的好習慣,同時這樣也就自然的為對象分配了職責。
6. 在編寫測試用例時,還需要考慮這個對象是如何使用的,這無疑就是在編寫對象的使用說明書了。
7. 由于以上兩點,可以使我們為了使對象或對象的方法便于測試,而降低了對象間的耦合,減少了依賴。
8. 進行測試驅動開發,還會使得我們傾向于領域驅動開發,而不是UI驅動或數據庫驅動,同時這也有利于將各層解耦。
另外有以下幾點值得思考:
1. 測試驅動開發提高總體效率的前提是什么?毫無疑問,從長遠來看,測試驅動開發提高了代碼質量,而軟件的成本往往從維護階段開始(譬如我們現在正在維護的這個項目,讓人欲死欲活的)。但是,我在進行TDD實踐時,確實比直接開發花費了更多時間,包括思考測試用例應該怎樣寫,比如測試持久層就想了半個多小時。
2. 另外,真的要關掉“異常時中斷”功能嗎?有幾次總是報ORA***:missing expression錯誤,我真的想設個斷點看看到底SQL語句成什么樣了。雖然錯誤的發生就在那兩三行中,或許某一個不超過20行的函數中,但是我還是花費了很多時間去反復測試,仔細看代碼,觀察到底在哪錯了,因為DUnit不會告訴你是哪行錯了。(或許有告訴,我不知道在哪而已)
3. 不知道為什么,我是將產品代碼所在的文件夾放在了search path中了,可是總會遇到產品代碼更新了,測試代碼那邊讀到的還不是最新結果。剛開始,我寫到會出現那個代碼已更改的提醒,之后代碼就同步了,可是后來重啟了一次Delphi就不行了。
4. 就是內存釋放問題,在測試代碼中和產品代碼中都要考慮內存釋放問題,很煩。
雖然在進行TDD實踐過程中,碰到了不少挫折,不過總體而言,我覺得驅動測試開發讓我在編寫測試用例時思考對象的工作方式,是一種漸進的思考過程。其實,我一直的編程習慣是,面對一個問題先花費一兩天時間思考,把各種關系都搞清楚了,然后在兩個小時內一氣呵成,由于思考的很清楚,因此錯誤也挺少的。Planned Design和進化式設計兩種方式都讓我感到獲益,XP之所以叫做極限編程,其中一個極限的部分可能就是其簡單設計的極限,不需要任何的架構設計,就根據用戶故事開始編程。不過我覺得我需要更多的實踐TDD,那些編寫測試用例遇到的困難,我想或許每個新手都會遇到,唯有多多實踐才能真正的提高。
5 大項目的思考
在大項目和大的團隊中推行TDD是有困難的。
1. 首先,TDD需要編寫產品代碼以外的測試代碼,很多程序員為了快速完成任務(有些人的時間只夠編寫產品代碼),不會愿意寫測試代碼的。雖然它從長遠考慮會有很多好處,但是現在的程序員有多少會想很遠呢(這也跟責任心有關)?可能等到維護時,我都走人了。
2. 其次,TDD需要開發人員有很強的設計能力,在這里我討論OO設計,不過我發現,在中國,程序員對OO都知之甚少。更甭說進行優秀的OO設計了。況且,Delphi不支持垃圾回收,習慣于“拖拉機”方式的程序員估計要造成很多混亂了。
3. 最后,比如我們這樣的大型項目,數百張數據表,上千個窗體,如何進行TDD,如果我自己沒做過,真是很難說服老板推薦這么干。雖然我堅信,這是可行的。
或許在您的項目中,已經成功地應用了TDD,那么希望您能夠分享您的經驗。如果您經歷的大型項目正在使用TDD,那么我們所有的讀者都將非常感興趣。
希望本文有任何不足之處,都請與我聯系,在我的Blog上留言或給我發郵件:sslaowan@gmail.com