Delphi XE3とPascal Mockでモックを使ったテストプログラム

この記事はDelphi Advent Calendar 2012に参加しています。

Delphi XE3でPascal Mockを使って、モックを使ったテストの方法を紹介します。

Pascal Mock

Pascal Mockは次のページからダウンロードできます。

Pascal Mockはインストールの必要はなく、プロジェクトにファイルを追加するだけで使用できます。

Delphi XE3で使用するときは、VariantsUtil.pasのConstArrayToVariantArrayを修正する必要がありました。

procedure ConstArrayToVariantArray(const AValues : array of const;
  var ADest: TVariantArray);
var
  i : Integer;
begin
  SetLength(ADest, Length(AValues));
  for i := Low(AValues) to High(AValues) do
  begin
    with AValues[i] do
    begin
      case VType of
        vtInteger: ADest[i] := VInteger;
        vtInt64: ADest[i] := VInt64^;
        vtBoolean: ADest[i] := VBoolean;
        vtChar: ADest[i] := VChar;
        vtExtended: ADest[i] := VExtended^;
        vtString: ADest[i] := VString^;
        vtPointer: ADest[i] := Integer(VPointer);
        vtPChar: ADest[i] := StrPas(VPChar);
        vtObject: ADest[i]:= Integer(VObject);
        vtAnsiString: ADest[i] := String(VAnsiString);
        vtCurrency: ADest[i] := VCurrency^;
        vtVariant: ADest[i] := VVariant^;
        vtInterface: ADest[i]:= Integer(VPointer);
        vtUnicodeString: ADest[i]:= String(VUnicodeString); //<=追加
      else
        raise Exception.Create ("invalid data type " + IntToStr(VType))
      end;
    end;
  end;
end;

これで付属のテストプログラムのテストが通るようになりました。
(この修正方法が正しいのか自信がありません。)

作成するテスト

次のようなテストを作成します。

■登場するクラス

  • 注文(TOrder)は商品と数量を一つ持つ。
  • 倉庫(TWarehouse)はいろいろな商品を持つ。

■ユースケース

  • 注文に対して倉庫で引当をしようとするとき
    • 倉庫に十分な商品があれば
      • 注文は引当てられる。
      • 倉庫の商品数は減る。
    • 倉庫に十分な商品がなければ
      • 注文は引当てられない。

倉庫に十分な商品があるときのテスト

モックを使ったテストでは、メインオブジェクトがモックのメソッドを想定に従って呼び出したかをチェックします。

注文(TOrder)クラスと倉庫クラスのモック(TWarehouseMock)のインスタンスを作成します。

const
  TALISKER: String = "talisker";
var
  Order: TOrder;
  Warehouse: TWarehouseMock;
begin
  Order := TOrder.Create(TALISKER, 50);
  Warehouse := TWarehouseMock.Create;

Orderに呼ばれる想定のメソッドを倉庫クラスのモックに登録します。

Warehouse.Expects("HasInventory").WithParams([TALISKER, 50]).Returns(True);
Warehouse.Expects("Remove").WithParams([TALISKER, 50]);

テスト対象のメインオブジェクトの処理を実行します。

Order.Fill(Warehouse);

テスト対象のメインオブジェクトの状態をチェックします。

CheckTrue(Order.Filled);

テスト対象がモックを想定に従って呼び出したかをチェックします。

Warehouse.Verify("Is everything ok?");

全体のコードは次のようになりました。

uses PascalMock, NoRefCountObject;

const
  TALISKER: String = "talisker";

procedure TestOrderInteraction.TestFillingRemovesInventoryIfInStock;
var
  Order: TOrder;
  Warehouse: TWarehouseMock;
begin
  Order := TOrder.Create(TALISKER, 50);
  Warehouse := TWarehouseMock.Create;

  //Orderに呼ばれるメソッドの想定
  Warehouse.Expects("HasInventory").WithParams([TALISKER, 50]).Returns(True);
  Warehouse.Expects("Remove").WithParams([TALISKER, 50]);

  //注文に対して倉庫で引当をしようとする
  Order.Fill(Warehouse);

  //注文は引き当てられているかをチェックする
  CheckTrue(Order.Filled);
  //想定にしたがってメソッドが呼び出しがされたかをチェックする
  Warehouse.Verify("Is everything ok?");

  Warehouse.Free;
end;

モックを使ったテストでは、商品数を減らすメソッド(Removeメソッド)が正しく呼ばれていれば良く、商品数が減っているかはチェックしません。

倉庫に十分な商品がないときのテスト

procedure TestOrderInteraction.TestFillingDoesNotRemoveIfNotEnoughInStock;
var
  Order: TOrder;
  Warehouse: TWarehouseMock;
begin
  Order := TOrder.Create(TALISKER, 51);
  Warehouse := TWarehouseMock.Create;

  //Orderに呼ばれるメソッドの想定
  Warehouse.Expects("HasInventory").Returns(False);

  //注文に対して倉庫で引当をしようとする
  Order.Fill(Warehouse);

  //注文は引き当てられていないかをチェックする
  CheckFalse(Order.Filled);
  //想定にしたがってメソッドが呼び出しがされたかをチェックする
  Warehouse.Verify("Is everything ok?");

  Warehouse.Free;
end;

IWarehouseインターフェース

倉庫はインターフェースのみ定義します。
倉庫の実装はありません。

type
  IWarehouse = interface
    function HasInventory(ItemName: String; Count: Integer): Boolean;
    procedure Remove(ItemName: String; Count: Integer);
  end;

今回はインターフェースを使った方法でモックを作成しています。
後で、仮想メソッドを使ったモックの作成方法を紹介します。

TOrderクラス

テストの対象である注文クラスは次のようになります。

簡単なクラスなので特に説明はありません。

uses Warehouse;

type
  TOrder = record
  private
    FItemName: String; //商品名
    FCount: Integer; //数量
    FFilled: Boolean; //引当
  public
    constructor Create(ItemName: String; Count: Integer);
    procedure Fill(Warehouse: IWarehouse);
    property  Filled : Boolean read FFilled write FFilled;
  end;

implementation

{ TOrder }

constructor TOrder.Create(ItemName: String; Count: Integer);
begin
  FItemName := ItemName;
  FCount := Count;
  FFilled := False;
end;

procedure TOrder.Fill(Warehouse: IWarehouse);
begin
  if Warehouse.HasInventory(FItemName, FCount) then
  begin
    //倉庫に十分な商品があれば
    FFilled := True;  //注文は引当てられる
    Warehouse.Remove(FItemName, FCount);  //倉庫の商品数は減る
  end
  else
  begin
    //倉庫に十分な商品がなければ
    FFilled := False; //注文は引当てられない
  end;
end;

TWarehouseMockクラス

テストに使用するモックです。
メソッドの呼び出しを基底クラスのTMockに委譲しています。

商品数をチェックする処理はありません。

type
  TWarehouseMock = class(TMock, IWarehouse)
    function HasInventory(ItemName: String; Count: Integer): Boolean;
    procedure Remove(ItemName: String; Count: Integer);
  end;

implementation

{ TWarehouseMock }

function TWarehouseMock.HasInventory(ItemName: String; Count: Integer): Boolean;
begin
  Result := AddCall("HasInventory").WithParams([ItemName, Count]).ReturnValue;
end;

procedure TWarehouseMock.Remove(ItemName: String; Count: Integer);
begin
  AddCall("Remove").WithParams([ItemName, Count]);
end;

仮想メソッドを使ったモックの作り方

インターフェースではなく、仮想メソッドを使った方法だと次のようになります。

インターフェースを使ったときと同じように、メソッドが呼ばれたらTMockに委譲します。

type
  TWarehouse = class
  public
    function HasInventory(ItemName: String; Count: Integer): Boolean; virtual; abstract;
    procedure Remove(ItemName: String; Count: Integer); virtual; abstract;
  end;

  TWarehouseMock = class(TWarehouse)
  private
    FMock: TMock; // a mock object which will check for us
  public
    constructor Create;
    destructor Destroy; override;
    function Expects(const ASignatureCall: string; AExpectedCalls: Integer = 1): TMockMethod;
    procedure Verify(const AMessage: string = "");

    function HasInventory(ItemName: String; Count: Integer): Boolean; override;
    procedure Remove(ItemName: String; Count: Integer); override;
  end;

implementation

{ TWarehouseMock }

constructor TWarehouseMock.Create;
begin
  inherited;
  FMock := TMock.Create;
end;

destructor TWarehouseMock.Destroy;
begin
  FMock.Free;
  inherited;
end;

function TWarehouseMock.Expects(const ASignatureCall: string;
  AExpectedCalls: Integer): TMockMethod;
begin
  Result := FMock.Expects(ASignatureCall, AExpectedCalls);
end;

function TWarehouseMock.HasInventory(ItemName: String; Count: Integer): Boolean;
begin
  Result := FMock.AddCall("HasInventory").WithParams([ItemName, Count]).ReturnValue;
end;

procedure TWarehouseMock.Remove(ItemName: String; Count: Integer);
begin
  FMock.AddCall("Remove").WithParams([ItemName, Count]);
end;

procedure TWarehouseMock.Verify(const AMessage: string);
begin
  FMock.Verify(AMessage);
end;

最後に

この記事では、Pascal Mockを使ったテストの方法を紹介しました。

オブジェクトの状態ではなく、オブジェクトの振る舞いをテストする方法がいかがでしたでしょうか。

モックを使えばテスト対象のクラス以外は実装する必要がなく、テスト対象のクラスに集中することができました。

Delphi用のモックライブラリを調べてみたところ、次のようなものがあるようです。

お使いの方がいらっしゃいましたら、使用感などを教えていただけるとうれしく思います。

参考

以下のページを参考にしました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください