この記事は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用のモックライブラリを調べてみたところ、次のようなものがあるようです。
お使いの方がいらっしゃいましたら、使用感などを教えていただけるとうれしく思います。
参考
以下のページを参考にしました。