- お知らせ -
  • 当wikiのプログラムコードの表示を直してみました(ついでに長い行があると全体が下にぶっ飛ぶのも修正)。不具合があればBBSまでご連絡下さい。

Delphi

はじめに Edit

DelphiでBDD(Behaviour Driven Development,振舞い駆動開発)をdSpecでやろうという話。

dSpec はDelphiでBDDを支援するライブラリです。rSpecなどのDelphi版です。dSpecは、BDS2006あたりから標準装備されたUnitTestライブラリdUnitの拡張として実装されており、GUIテストランナーなどはdUnitのものをそのまま使えます。

dSpecは、delphiXtreme » Blog Archive » dSpec - BDD for Delphi よりダウンロードできます。
dSpec.rarをダウンロードのこと。

BDDはTDDも含むので、基本的なところはSoftware Testing/TDD(テスト駆動開発)も参考にしてみてください。

BDDはビヘイビア駆動開発@wikipediaの欄も読んでみるとよいでしょう。

※dSpecがどうもメモリリークしまくるので悩んでます orz。 BDS2006で確認中。

BDDやってみる例 Edit

1.座標の足し算を参考に簡単な振舞いの記述をdSpecでしてみます。

動作確認はBDS2006にて確認しています。

基本的なこと Edit

開発の流れですが、Software Testing/TDD(テスト駆動開発)にも書きましたが、

  1. 考える(Think)
  2. まずはテスト(BDDなので振舞い)を書いてわざと失敗させる(Red)
  3. 実装を書いてテストを成功させる(Green)
  4. リファクタリングする。テストはもちろん成功させる(Refactor)
  5. 1に戻る

という基本を守って行います。

振舞いクラスの作成 Edit

「ファイル→新規作成→その他」にて新規作成ダイアログを開き、「Unit Test→テストプロジェクト」にて、テストプロジェクトを作成します。
名前は、PointSpecTestなどとします。
(旧バージョンでできない場合は、まずは、dUnitをインストールしてください)

次に、振舞いをおく、 spec フォルダをつくります。

そして、IDEでユニットを新規作成します。
名前は MyPointSpecUnit.pas などとしてspecフォルダに保存します。

dSpec/sourceにライブラリパスを通しておき、
dSpec を uses します。

interface
uses dSpec;

クラスごとにTContextを継承し、仕様(Spec)クラスを宣言します。
クラス名は、MyPointSpecとしました(接頭子Tは省くようです)

type
  MyPointSpec = class(TContext)
  end;

ユニットのinitialization節では、各SpecをRegisterSpecで登録します。

initialization
  RegisterSpec(MyPointSpec.Suite);

これで実行してみましょう。
まだテストは走らせることはできませんが、PointSpecの名前は表示はされたかと思います。

NewSpecClass.jpg

新規に実装クラスを作る Edit

実装クラスを宣言して作ってみます。

ユニットを新規作成し、MyPointUnit.pasなどとして保存します。

その後、TMyPoint を作成してみましょう。

type
  TMyPoint = class
  end;

spec/MyPointSpecUnit.pasでは、
MyPointUnit.pas を uses します。

uses dSpec, MyPointUnit;

最初の失敗するテスト(振舞い)を書く Edit

まず、TMyPointに座標を取得する GetX, GetY を作りたいと思います。
その振舞いをMyPointSpec内に書てみましょう。
初期値は、X = 3, Y = 4とします。

最初は振舞いを書き、わざと失敗させます。

さて、MyPointSpecのPublished に ShouldGetXYCorrectly メソッドを定義します。
メソッドの内容は以下のような感じでdSpec風に書きます。

  published
    procedure ShouldGetXYCorrectly;

 :

procedure MyPointSpec.ShouldGetXYCorrectly;
var MyPoint: TMyPoint;
begin
  MyPoint := TMyPoint.Create;
  try
    Specify.That(MyPoint.GetX, 'TMyPoint.GetX').Should.Equal(3);
    Specify.That(MyPoint.GetY, 'TMyPoint.GetY').Should.Equal(4);
  finally
    MyPoint.Free;
  end;
end;

実行すると、失敗!といきたいところですが、コンパイルエラーで実行できませんよね。

そこで、TMyPoint.GetX, TMyPoint.GetY を適当に実装します。

  TMyPoint = class
  public
    function GetX: Integer;
    function GetY: Integer;
  end;

 :

{ TMyPoint }
function TMyPoint.GetX: Integer;
begin
  Result := 0;
end;

function TMyPoint.GetY: Integer;
begin
  Result := 0;
end;

実行すると、XとYに3と4を期待しているのに双方とも0が返ってくるので、当然失敗します。これでRedの状態です。

FirstFailed.jpg

インチキして最初の失敗を成功にする Edit

さて、このテストをインチキして(TDDでいうFake It)成功させましょう
まずは、インチキしてテストを通すようにTMyPoint.GetX,TMyPoint.GetYを修正します。

{ TMyPoint }
function TMyPoint.GetX: Integer;
begin
  Result := 3;
end;

function TMyPoint.GetY: Integer;
begin
  Result := 4;
end;

上手くいきましたか?

CheatSuccess.jpg

ちゃんとした実装をする Edit

今度は、ちゃんとした実装をします。そしてテストを成功させましょう。

まず、TMyPointのコンストラクタで初期値を与えるようにします。
そして、TMyPointのフィールドにもたせてそれをGetX,GetYで返すようにします。

  private
    FX, FY: Integer;
  public
    constructor Create(X, Y: Integer);

 :

{ TMyPoint }
constructor TMyPoint.Create(X, Y: Integer);
begin
  FX := X;
  FY := Y;
end;

function TMyPoint.GetX: Integer;
begin
  Result := FX;
end;

function TMyPoint.GetY: Integer;
begin
  Result := FY;
end;

MyPointSpecでは初期値を与えるように修正します。

 procedure MyPointSpec.ShouldGetXYCorrectly;
 var MyPoint: TMyPoint;
 begin
-   MyPoint := TMyPoint.Create;
+   MyPoint := TMyPoint.Create(3, 4);
   try
     Specify.That(MyPoint.GetX, 'TMyPoint.GetX').Should.Equal(3);
     Specify.That(MyPoint.GetY, 'TMyPoint.GetY').Should.Equal(4);
   finally
     MyPoint.Free;
   end;
 end;

※パッチ形式で書き表しています。-が元の行、+が修正した行です。

この間、テストを実行してみてきちんと成功させるようにした状態で修正します。

実行して、成功するか確認してください

CheatSuccess.jpg

リファクタリングする Edit

何か、FX, FYの構造はWindows.TPointで代用できるような気がしてきました。
そこで、リファクタリングします。

以下のような感じに直してみます。

uses Windows;
type
  TMyPoint = class
  private
    FPoint: TPoint;
 :
{ TMyPoint }
constructor TMyPoint.Create(X, Y: Integer);
begin
  FPoint.X := X;
  FPoint.Y := Y;
end;

function TMyPoint.GetX: Integer;
begin
  Result := FPoint.X;
end;

function TMyPoint.GetY: Integer;
begin
  Result := FPoint.Y;
end;

これらを修正するときは、
テストを通して成功の状態で修正(リファクタリング)します

CheatSuccess.jpg

テストで常にチェックしていると安心して、リファクタリングができますよね?

さらなる仕様追加と2度目の失敗 Edit

新しい仕様として、Moveメソッドを追加してみようと思います。

MyPointSpecにShouldMoveを書いてみます。

type
  MyPointSpec = class(TContext)
   :
  published
    procedure ShouldGetXYCorrectly;
    procedure ShouldMove;
  end;

 :

procedure MyPointSpec.ShouldMove;
var MyPoint: TMyPoint;
begin
  MyPoint := TMyPoint.Create(3, 4);
  try
    MyPoint.Move(10, -5);
    Specify.That(MyPoint.GetX, 'TMyPoint.GetX after Move').Should.Equal(13);
    Specify.That(MyPoint.GetY, 'TMyPoint.GetY after Move').Should.Equal(-1);
  finally
    MyPoint.Free;
  end;
end;

コンパイルが通らないので、適当に形だけ実装します。

type
  TMyPoint = class
    :
    procedure Move(X, Y: Integer);
  end;
 :
procedure TMyPoint.Move(X, Y: Integer);
begin

end;

実装が完了していないので、実行したら失敗しますよね?

SecondFailure.jpg

インチキしてテストを通す、2度目 Edit

また、インチキして(Fake It)テストを通します。

procedure TMyPoint.Move(X, Y: Integer);
begin
  FPoint.X := 13;
  FPoint.Y := -1;
end;

実行します。
OK.テストは通りますよね?

SecondFakeIt.jpg

正しく実装する、2度目。 Edit

TMyPoint.Moveをきちんと実装してみましょう。

procedure TMyPoint.Move(X, Y: Integer);
begin
  FPoint.X := FPoint.X + X;
  FPoint.Y := FPoint.X + Y;
end;

さて、テスト実行。
あれ?テストが通りませんね……。

SecondFailure2.jpg

どうやら、コピペした時にFPoint.Yのところを修正し忘れたようです。
修正します。

 procedure TMyPoint.Move(X, Y: Integer);
 begin
   FPoint.X := FPoint.X + X;
-  FPoint.Y := FPoint.X + Y;
+  FPoint.Y := FPoint.Y + Y;
 end;

※パッチ形式で書き表しています。-が元の行、+が修正した行です。

上手くいったようです。

SecondFakeIt.jpg

もし可能なら、リファクタリングをやります。
この段階では特に必要ないと思います。

共通部分をSetUp,TearDownにまとめてみる Edit

MyPointSpec.ShouldGetXYCorrectly や MyPointSpec.ShouldMoveには、

MyPoint := TMyPoint.Create(3, 4);

や、

MyPoint.Free

という共通の部分が2つ出てきました。

今後も機能を追加する場合も考え、この生成部分を共通部分として、
各テスト前の処理を書くSetUp、各テスト後の処理を書くTearDownにまとめてみましょう。

まずは、MyPointをMyPointSpecクラスのフィールドメンバでFMyPointを宣言してやります。

  MyPointSpec = class(TContext)
  private
    FMyPoint: TMyPoint;

そして、protected に SetUpとTearDownメソッドを定義して、TMyPointをテスト実行時に必ず生成、開放してやるようにします。

  protected
    procedure SetUp; override;
    procedure TearDown; override;

:

{ MyPointSpec }
procedure MyPointSpec.SetUp;
begin
  FMyPoint := TMyPoint.Create(3, 4);
end;

procedure MyPointSpec.TearDown;
begin
  FMyPoint.Free;
end;

あとは、MyPointSpec.ShouldGetXYCorrectly と MyPointSpec.ShouldMoveを修正します。
具体的には、MyPointをFMyPointに置き換え、生成、開放部分は削除します。

procedure MyPointSpec.ShouldGetXYCorrectly;
begin
  Specify.That(FMyPoint.GetX, 'TMyPoint.GetX').Should.Equal(3);
  Specify.That(FMyPoint.GetY, 'TMyPoint.GetY').Should.Equal(4);
end;

procedure MyPointSpec.ShouldMove;
begin
  FMyPoint.Move(10, -5);
  Specify.That(FMyPoint.GetX, 'TMyPoint.GetX after Move').Should.Equal(13);
  Specify.That(FMyPoint.GetY, 'TMyPoint.GetY after Move').Should.Equal(-1);
end;

テストしてみます。
OK?成功しましたか?

SecondFakeIt.jpg

課題 Edit

例えば以下の課題を試してみましょう。

  • TMyPointにTMyPointを加算する TMyPoint.Plus メソッドの実装
  • TMyPointにTMyPointを減算する TMyPoint.Minus メソッドの実装
  • TMyPointとTMyPointを比較する TMyPoint.Equals メソッドの実装

No comment. Comments/Delphi/dSpec(BDD)?

Name:


Attach file: fileSecondFailure2.jpg 58 download [Information] fileSecondFakeIt.jpg 56 download [Information] fileSecondFailure.jpg 57 download [Information] fileCheatSuccess.jpg 46 download [Information] fileFirstFailed.jpg 45 download [Information] fileNewSpecClass.jpg 57 download [Information]
Front page   Edit Freeze Diff Backup Upload Copy Rename Reload   New Pages Search Recent changes   Help   RSS of recent changes
Last-modified: 2008-11-12 Wed 20:06:18 JST (3932d)