零雨其蒙's Blog

做优秀的程序员
随笔 - 59, 文章 - 13, 评论 - 58, 引用 - 0
数据加载中……

零雨其蒙:Practicing Test-Driven Development by Example Using Delphi

 

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-inNew 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的属性(propertyqry。代码如下:

{ 作者:零雨其蒙

创建时间: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,然后放置一个叫做sgdRetireStringGrid,这时就是通过列表循环赋值到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

 

posted on 2007-05-24 22:31 零雨其蒙 阅读(1306) 评论(1)  编辑  收藏 所属分类: 面向对象理论与实践

评论

# re: 零雨其蒙:Practicing Test-Driven Development by Example Using Delphi  回复  更多评论   

精神可嘉。还需要去理解TDD(AGILE)的内涵和本质。读了您的代码,觉得你的理论和实践还是有不小的差距(恕我直言),有为了测试而强行测试(可能表达得不准确)的感觉。

只有注册用户登录后才能发表评论。


网站导航: