<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    零雨其蒙'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

        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的屬性(propertyqry。代碼如下:

    { 作者:零雨其蒙

    創建時間: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,然后放置一個叫做sgdRetireStringGrid,這時就是通過列表循環賦值到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

     

    posted on 2007-05-24 22:31 零雨其蒙 閱讀(1324) 評論(1)  編輯  收藏 所屬分類: 面向對象理論與實踐

    評論

    # re: 零雨其蒙:Practicing Test-Driven Development by Example Using Delphi  回復  更多評論   

    精神可嘉。還需要去理解TDD(AGILE)的內涵和本質。讀了您的代碼,覺得你的理論和實踐還是有不小的差距(恕我直言),有為了測試而強行測試(可能表達得不準確)的感覺。
    主站蜘蛛池模板: 国产成人va亚洲电影| 久九九精品免费视频| 中文文字幕文字幕亚洲色| 免费99热在线观看| h在线观看视频免费网站| 亚洲免费日韩无码系列| 亚洲第一成年免费网站| 亚洲一区中文字幕| 亚洲国产精品自在在线观看| 四虎永久在线精品免费观看地址| 18勿入网站免费永久| 99re热精品视频国产免费| www成人免费观看网站| 色婷婷综合缴情综免费观看| 亚洲一区欧洲一区| 黄色一级免费网站| 国产精品成人亚洲| 在线观看免费黄色网址| 国产亚洲视频在线播放大全| 中文字幕视频免费在线观看| 一级做受视频免费是看美女| 久久午夜羞羞影院免费观看| 在线观看免费大黄网站| www.黄色免费网站| 久草视频免费在线观看| 国产一区在线观看免费| 久久久久亚洲Av片无码v| 久久久青草青青亚洲国产免观| 亚洲成年看片在线观看| 国产精品极品美女免费观看| 日韩伦理片电影在线免费观看| 精品少妇人妻AV免费久久洗澡| 免费理论片51人人看电影| 久久精品国产亚洲Aⅴ蜜臀色欲| 亚洲&#228;v永久无码精品天堂久久 | 亚洲成a人片77777老司机| 亚洲成av人片天堂网无码】| 秋霞人成在线观看免费视频 | 亚洲AV无码一区二区三区电影 | 亚洲性无码av在线| 亚洲国产成人久久三区|