2008 年 10 月 的封存

第3章 使用DBX4的測試框架類別(1)

DBX框架中除了處理資料庫的類別之外,其實還包含了許多有用的輔助類別,這些輔助類別除了可以幫助開發人員撰寫資料處理功能之外,也可以幫助開發人員建立測試資料庫,測試資料表以及自動建立測試資料等一般在開發資料庫應用程式經常需要進行的工作。DBX框架甚至擴展了DUnit的測試驅動類別讓開發人員可以建立資料庫測試案例以測試應用程式以及資料是否正確,如此一來可以讓開發人員結合測試驅動開發(TDD)以及資料庫開發的工作。
本章的討論重點就是為各位介紹這些非常有用的相關類別,在解釋了這些類別之後本章將使用一些範例來展示如何使用這些有用的類別,現在讓我們從最基礎的TDBXCustomDataGenerator類別開始說起。

3-1 可客製化的自動建立測試資料類別 – TDBXCustomDataGenerator
TDBXCustomDataGenerator類別從它的名稱上我們就可以猜到這個類別提供的功能是能夠進行客製化工作的資料產生類別,沒錯,TDBXCustomDataGenerator提供的服務就是和上一章討論的TDBXMetaDataProvider類別合作,一起在開發人員指定的資料表中自動建立測試資料並且進行各種測試的工作。
下面的表格整理了TDBXCustomDataGenerator提供的公用(public)方法和特性的說明,開發人員可以藉由呼叫這些方法或是存取這些特性來自動建立測試資料:
http://blufiles.storage.live.com/y1pX_1ak1MLTU9iePlc4goIvo9kCPt-FZ9jTOG0TkVV9U-Ko9713VFg55uZ1bnnr-Km

在上一章中我們詳細的說明了TDBXMetaDataProvider類別的功能,由於TDBXMetaDataProvider類別可提供處理資料庫物件的功能,因此TDBXCustomDataGenerator類別可以藉由TDBXMetaDataProvider提供的服務來自動建立測試資料庫,測試資料表和測試資料表中的欄位,之後再根據這些資訊來建立正確的Insert SQL敘述在測試資料表中自動建立測試資料。
此外,當TDBXCustomDataGenerator自動產生測試資料時,是使用TDBXDataGeneratorColumn類別來產生測試資料表欄位中的數值,而TDBXDataGeneratorColumn是一個抽象類別,它的實體類別由其他10多個繼承類別來實作以提供特定的資料型態欄位的數值,開發人員可以直接使用這些實體類別來自動產生欄位資料,也可以再從這些實體類別繼承下來實作客製化的TDBXDataGeneratorColumn類別來產生符合應用程式需要的測試資料,在稍後的小節中我們將會說明TDBXDataGeneratorColumn和它相關的實體類別。
雖然開發人員可以直接使用TDBXCustomDataGenerator類別,但是DBX框架提供了一個TDBXCustomDataGenerator的繼承類別: TDBXDataGenerator,開發人員應該盡量使用TDBXDataGenerator而不是直接使用TDBXCustomDataGenerator類別,因為TDbxDataGenerator又額外提供許多驗證資料的服務,因此接下來讓我們說明TDbxDataGenerator類別的功能。

3-2 DBX4自動建立測試資料類別 –TDbxDataGenerator
TDBXDataGenerator是TDBXCustomDataGenerator的子類別,它在TDBXCustomDataGenerator提供的服務之外主要是加上驗證資料的服務。TDBXDataGenerator藉由提供許多靜態類別的方法來提供驗證資料的服務,這些方法都是以Validate為開頭的方法,例如ValidateAnsiString,ValidateWideString等,下面即是TDBXDataGenerator的類別宣告:
  TDbxDataGenerator = class(TDBXCustomDataGenerator)
  private
    FParam: TParam;
    //Remove once all drivers support Fractions (milliseconds) for timestamp (255605, 255767, 255768, 255769)
    function GetIsFractionsSupported: Boolean;
    procedure SetParameter(Ordinal: Integer; Row: Integer; Param: TParam);
    procedure ValidateParam(Ordinal: Integer; Row: Integer; Param: TParam);
  public
    constructor Create;
    destructor Destroy; override;
    class procedure ValidateAnsiString(Value1, Value2: String); static;
    class procedure ValidateWideString(Value1, Value2: String); static;
    class procedure ValidateBoolean(Value1, Value2: Boolean); static;
    class procedure ValidateInt64(Value1, Value2: Int64); static;
    class procedure ValidateDouble(Value1, Value2: Double); static;
    class procedure ValidateBcd(Value1, Value2: TBcd); static;
    class procedure ValidateDate(Value1, Value2: TDBXDate); static;
    class procedure ValidateTime(Value1, Value2: TDBXTime); static;
    class procedure ValidateTimestamp(Value1, Value2: TSQLTimeStamp); static;
    class procedure ValidateBytes(Value1, Value2: TBytes); static;
    class function BytesEquals(const Value1: TBytes; const Value2: TBytes; Count: Integer): Boolean; static;
    class procedure ReadBytesFromStream(Value: TDBXValue; var Buffer: TBytes);

    function ValidateRow(const Reader: TDBXReader; Row: Integer): Boolean; overload;
    function ValidateRow(const DataSet: TDataSet; Row: Integer): Boolean; overload;
    procedure ValidateParameters(const Command: TDBXCommand; Row: Integer);
    procedure ValidateParams(const Params: TParams);
    procedure AddParameters(const Command: TDBXCommand); overload;
    procedure AddParameters(const SQLQuery: TSQLQuery); overload;
    {$IF DEFINED(CLR)}
    procedure AddParameters(const Command: DbCommand); overload;
    {$IFEND}
    procedure SetInsertParameters(const Command: TDBXCommand; Row: Integer); overload;
    procedure SetInsertParameters(const SQLQuery: TSQLQuery; Row: Integer); overload;
    {$IF DEFINED(CLR)}
    procedure SetInsertParameters(const Command: DbCommand; Row: Integer); overload;
    {$IFEND}
    procedure PopulateParams(Params: TParams);
    procedure PopulateDataset(DataSet: TDataSet; RowCount: Integer);
    //Remove once all drivers support Fractions (milliseconds) for timestamp (255605, 255767, 255768, 255769)
    property IsFractionsSupported: Boolean read GetIsFractionsSupported;
  end;

當開發人員使用TDBXCustomDataGenerator建立了測試資料之後,就可以再藉由TDbxDataGenerator的服務來測試資料,因此開發人員直接標準專家TDbxDataGenerator類別就可以同時進行這兩個工作。
從上面的各個ValidateXXXX方法我們可以猜到,開發人員能夠藉由呼叫這些不同的ValidateXXXX方法來測試各種不同的資料型態的欄位數值,至於TDbxDataGenerator類別其他的一些方法則是使用來在其他物件中根據TDbxDataGenerator代表的資料表的欄位在其他物件中建立相對應的物件,例如下面的AddParameters方法:

    procedure AddParameters(const Command: TDBXCommand); overload;


它的功能就根據TDbxDataGenerator代表的資料表欄位在它的參數TDBXCommand物件中建立TDBXParameter物件。
至於SetInsertParameters方法:

    procedure SetInsertParameters(const Command: TDBXCommand; Row: Integer); overload;

則是根據TDbxDataGenerator代表的資料表欄位數值設定它的參數TDBXCommand物件中TDBXParameter物件的數值。
說明了TDBXCustomDataGenerator和TDbxDataGenerator這兩個類別之後,讓我們來看看如何使用它們。

3-3 使用TDbxDataGenerator類別
現在讓我們在一個資料表中來自動產生測試資料並且使用TDbxDataGenerator提供的驗證方法來驗證資料,從這個範例中我們將可以瞭解如何使用TDBXCustomDataGenerator和TDbxDataGenerator類別。

3-3-1 使用TDBXCustomDataGenerator自動產生測試資料
在第2章中我們使用了DBX的Meta類別展示了如何建立資料表DBXTESTTABLE,因此讓我們繼續使用這個範例資料表來展示如何在其中動建立測試資料。DBXTESTTABLE是一個擁有如下結構的資料表:
 
http://blufiles.storage.live.com/y1p0y6rO7sMZt3Fx3VQG_5lULFUN1VAyvaYHe2V8Upn4Gm--FATaxbbrdh_8Vb4nxiR
圖3-1 測試資料表DBXTESTTABLE的結構

要在其中產生測試資料,我們需要建立TDbxDataGenerator物件,但首先我們需要先取得DBX連結物件,再取得DBX元物件之後才能使用TDbxDataGenerator物件。因此首先呼叫『取得資料來源』連結使用第一章介紹的TDBXConnectionFactory類別來連結DBX資料來源:
procedure TForm5.btn建立測試資料重構的版本Click(Sender: TObject);
begin
  取得資料來源連結;
end;

取得了連結資料來源的TDBXConnection物件之後,就可以呼叫『建立測試資料』方法使用TDbxDataGenerator來自動建立測試資料了:

procedure TForm5.btn建立測試資料重構的版本Click(Sender: TObject);
begin
  取得資料來源連結;
  建立測試資料;
end;

『建立測試資料』方法的工作就是使用TDbxDataGenerator自動建立測試資料,因此它需要進行的工作如下:
1.    藉由TDBXMetaDataProvider取得要建立測試資料的資料表的結構
2.    根據步驟1的結果建立正確的TDbxDataGenerator物件
3.    呼叫TDbxDataGenerator物件的方法建立資料

下面的程式碼中顯示了如何完成上述的兩個步驟,005行的dbx4Conn即是前面呼叫『取得資料來源』方法而取得的TDBXConnection物件,007行的『取得DBXMetaProvider』即可取得TDbxDataGenerator物件,接著009行呼叫『資料表存在嗎』方法來判斷資料庫中是否存在要建立測試資料的資料表TESTTABLENAME。
001    procedure TForm5.建立測試資料;
002    var
003      aProvider: TDBXMetaDataProvider;
004    begin
005      if (Assigned(dbx4Conn)) then
006      begin
007        aProvider := 取得DBXMetaProvider(dbx4Conn);
008        try
009          if (資料表存在嗎(aProvider, TESTTABLENAME)) then
010          begin
011          end;
012        finally
013          aProvider.Free;
014        end;
015      end;
016    end;

現在有了TDBXConnection和TDBXMetaDataProvider這兩個必要的物件之後下面的013行建立TDbxDataGenerator物件,014行設定它的目標資料表名稱,015行設定它連結的TDBXMetaDataProvider物件,接著016行呼叫加入『欄位物件』方法在TDbxDataGenerator物件中建立測試資料相對應的欄位物件:

001    procedure TForm5.建立測試資料;
002    var
003      aProvider: TDBXMetaDataProvider;
004      DataGenerator: TDbxDataGenerator;
005    begin
006      if (Assigned(dbx4Conn)) then
007      begin
008        aProvider := 取得DBXMetaProvider(dbx4Conn);
009        try
010          if (資料表存在嗎(aProvider, TESTTABLENAME)) then
011          begin
012            try
013              DataGenerator := TDBXDataGenerator.Create;
014              DataGenerator.TableName := TESTTABLENAME;
015              DataGenerator.MetaDataProvider := aProvider;
016              加入欄位物件(aProvider, DataGenerator);
017            finally
018              Reader.Free;
019              DataGenerator.Free;
020            end;
021          end;
022        finally
023          aProvider.Free;
024        end;
025      end;
end;

為什麼需要執行上面『欄位物件』方法呢? 這是因為TDbxDataGenerator在資料表中自動建立測試資料時,它需要知道測試資料表每一個欄位的型態以便呼叫稍後會介紹的TDBXDataGeneratorColumn的衍生類別物件來建立測試資料,例如假設測試資料表中的第一個欄位是整數型態,那麼TDbxDataGenerator便會呼叫我們在TDbxDataGenerator物件中加入的TDBXDataGeneratorColumn的衍生類別物件來自動產生整數值。也許在我們解釋了『欄位物件』方法是如何工作之後讀者會更瞭解這個工作原理。

下面即是『欄位物件』方法的實作程式碼,為了節省篇幅因此我只列出了測試資料表欄位使用的資料型態,由於測試資料表的欄位型態只使用了整數,UnicodeString和日期型態,因此在045~057的case敘述中我判斷這些資料型態,在實際的程式碼中是應該判斷所有DBX支援的欄位資料型態的。OK,現在就讓我們說明『欄位物件』方法是如何工作的。

在041行中呼叫『取得欄位物件』方法藉由TDBXMetaDataProvider物件來取得測試資料表中所有的欄位物件,接著進入while迴圈一一的判斷每一個欄位物件的資料型態來建立相對應的TDBXDataGeneratorColumn的衍生類別物件來產生欄位值,例如如果測試資料表的欄位型態是整數型態,那麼就在007行先建立TDBXInt32Column物件,再於009行建立TDBXInt32SequenceGenerator物件並且加入到TDbxDataGenerator物件中,因此稍後當我們使用TDbxDataGenerator物件來產生測試資料時,TDbxDataGenerator物件中就會在測試資料表的整數型態欄位中使用TDBXInt32SequenceGenerator物件來自動產生測試欄位值。相同的,015行的『建立字串欄位』函式是使用TDBXWideStringSequenceGenerator來建立UnicodeString欄位的測試值,027行的『建立日期欄位』函式是使用TDBXDateSequenceGenerator物件在日期型態的欄位中建立測試值。

001    procedure TForm5.加入欄位物件(aProvider : TDBXMetaDataProvider; DataGenerator: TDbxDataGenerator);
002   
003      function 建立整數欄位(cName : string) : TDBXInt32SequenceGenerator;
004      var
005        col : TDBXInt32Column;
006      begin
007        col := TDBXInt32Column.Create(cName);
008        try
009          Result := TDBXInt32SequenceGenerator.Create(col);
010        finally
011          FreeAndNil(col);
012        end;
013      end;
014   
015      function 建立字串欄位(cName : string; iLength : Integer) : TDBXWideStringSequenceGenerator;
016      var
017        col : TDBXUnicodeVarCharColumn;
018      begin
019        col := TDBXUnicodeVarCharColumn.Create(cName, iLength);
020        try
021          Result := TDBXWideStringSequenceGenerator.Create(col);
022        finally
023          FreeAndNil(col);
024        end;
025      end;
026   
027      function 建立日期欄位(cName : string) : TDBXDateSequenceGenerator;
028      var
029        col : TDBXDateColumn;
030      begin
031        col := TDBXDateColumn.Create(cName);
032        try
033          Result :=TDBXDateSequenceGenerator.Create(col);
034        finally
035          FreeAndNil(col);
036        end;
037      end;
038    var
039      cols : TDBXColumnsTableStorage;
040    begin
041      cols := 取得欄位物件(aProvider, TESTTABLENAME);
042   
043      while (cols.InBounds) do
044      begin
045        case cols.DbxDataType of
046          TDBXDataTypes.Int32Type :
047          begin
048            DataGenerator.AddColumn(建立整數欄位(cols.ColumnName));
049          end;
050          TDBXDataTypes.WideStringType :
051          begin
052            DataGenerator.AddColumn(建立字串欄位(cols.ColumnName, cols.Precision));
053          end;
054          TDBXDataTypes.DateType :
055          begin
056            DataGenerator.AddColumn(建立日期欄位(cols.ColumnName));
057          end;
058        end;
059        cols.Next;
060      end;
end;

現在我們已經建立了正確的TDbxDataGenerator物件之後,接著下來的工作就是完成上面的第3個步驟的工作,首先我們如第一章所述建立TDBXCommand物件來準備執行新增資料的SQL敘述,如下面001行所示。

001    Command := dbx4Conn.CreateCommand;
002    Command.Text := DataGenerator.CreateParameterizedInsertStatement;
003    DataGenerator.AddParameters(Command);
004    for iRow := 1 to RowCount do
005    begin
006      DataGenerator.SetInsertParameters(Command, iRow);
007      Command.ExecuteUpdate;
end;

接著在在002行呼叫TDbxDataGenerator的CreateParameterizedInsertStatement方法在TDBXCommand物件中產生新增資料的SQL敘述,例如在這個測試資料表中產生如下的SQL敘述:

INSERT INTO DBXTESTTABLE ("序號","研討會名稱","主講人","日期") VALUES (?,?,?,?)

013行呼叫AddParameters方法為TDBXCommand物件中的每一個以『?』代表的動態參數設定正確的資料型態。
最後004的迴圈就是重點了,我們在006行呼叫TDbxDataGenerator的SetInsertParameters方法來實際的設定每一個動態參數的測試值,而測試值則是由前面『加入欄位物件』方法在TDbxDataGenerator物件中加入的欄位物件來真正的產生。
執行上面的程式碼之後,我們可以在測試資料表TESTTABLENAME之中看到如下自動產生的測試資料:

http://blufiles.storage.live.com/y1p8_nyyIyAMo7On1IfJ5fVpFLoe9t_UerMp66HxZjJ69lV-vZwSgShua81ZDFPsRX5
圖3-2 TDbxDataGenerator在測試資料表中自動產生的測試資料

從上面的討論中我們可以瞭解了DBX框架中的TDbxDataGenerator類別提供了自動化的機制讓我們在開發資料庫應用程式時能夠把許多的工作自動化,讓我們在開發資料庫應用程式時有更多的機制幫助我們撰寫出品質更高的程式碼。
當然,我知道讀者現在會問TDbxDataGenerator產生的資料是沒有意義的,能夠使用TDbxDataGenerator產生更實際的測試資料嗎? 這是當然的,DBX框架提供的資料庫測試是提供一個可用的框架讓開發人員使用,開發人員在瞭解了這個機制之後當然可以使用這些相關的類別進行客製化的工作,也可以藉由掌握了這些原理之後來產生實際的測試資料。
例如筆者可以藉由稍後討論的TDBXDataGeneratorColumn衍生類別在測試資料表中自動產生客製化的測試資料,如下所示:
 
http://blufiles.storage.live.com/y1px7zbP7LY1TjUJo0p8NgqwYnGDqRjBH9SnCiAetEcFP7ExUtSfn1IoLeK2D0502yy
圖3-3 在測試資料表中自動產生客製化的測試資料

藉由TDBXDataGeneratorColumn衍生類別我們可以產生任何實際的測試資料,而且完全自動化,接著再結合稍後介紹的TDBXTestCase類別,開發人員可以在開發資料庫應用程式的同時使用TDD的開發模式。

3-3-2 使用TDbxDataGenerator驗證資料
『待續』

廣告

5 則迴響