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
www.blogjava.net/sslaowan
}
function TRetire.getretireAwokeParaList: TObjectList;
var paraList:TObjectList;
begin
end;
这是一个空壳,没有实际的内容,然后切换到HRTest工程,会出现下面的对话框。
在HR工程做了任何改动,保存后,都会在HRTest中有提醒。
可能很多人从来没见过测试用例长什么样子,下面就给出一个完整的例子。
{ 作者:零雨其蒙
创建时间:2007-5-24
Blog:blog.csdn.net/sslaowan
www.blogjava.net/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
www.blogjava.net/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
www.blogjava.net/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
www.blogjava.net/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
www.blogjava.net/sslaowan
}
procedure TTestRetire.testConnect;
begin
check(_session.Connected=true);
end;
之后再进行测试,绿条终于出现了!
我在之前还犯了错误,总是出现红条。后来才发现原来对象创建没搞清楚。这个错误在我编写Java程序时也犯过,看来一定要注意阿。
{ 作者:零雨其蒙
创建时间:2007-5-24
Blog:blog.csdn.net/sslaowan
www.blogjava.net/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
www.blogjava.net/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
www.blogjava.net/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
www.blogjava.net/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
www.blogjava.net/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