モジュールの例
... type TTestDB1 = class(TTestCase) protected public procedure SetUp; override; procedure TearDown; override; published procedure TestDB1_1; procedure TestDB1_2; end; ... implementation ... procedure TTestDB1.SetUp; begin inherited; // connect to DB end; procedure TTestDB1.TearDown; begin // disconnect from DB inherited; end; ... initialization RegisterTest(TTestDB1.Suite); end.

呼び出しスキームは次のとおりです。
-- TTestDB1.SetUp ---- TTestDB1.TestDB1_1 -- TTestDB1.TearDown -- TTestDB1.SetUp ---- TTestDB1.TestDB1_2 -- TTestDB1.TearDown
さらに、データベースに接続する前に、必要な構造でデータベースを作成する必要があることがデータベースで発生する場合があります。
この問題を解決するために、 dUnitにはTTestSetupクラスがあります(TTestExtensionsモジュールで説明)。
実際、
ITest
と同じ
ITest
インターフェイス、つまり同じスキーム、SetUp、Test ...、TearDownを実装していますが、テストを呼び出す代わりに、作成時に指定されたテストケース全体が呼び出されます。 つまり モジュールの変更:
uses ... TestExtensions; type TTestDBSetup = class(TTestSetup) public procedure SetUp; override; procedure TearDown; override; // published- TTestSetup end; TTestDB1 = ... ... implementation ... initialization RegisterTest(TTestDBSetup.Create(TTestDB1.Suite)); end.
呼び出しスキームを取得します:
-- TTestDBSetup.SetUp ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_1 ---- TTestDB1.TearDown ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_2 ---- TTestDB1.TearDown -- TTestDBSetup.TearDown

本質的に、これはスイート+テストケーススキーマです。 したがって、TTestDBSetup.SetUpでデータベースへの接続を確立すると、TestDB1_1およびTestDB1_2を実行する前にこれを1回だけ行います。
データベースへの接続を必要とするテストを含むテストケースが1つしかない場合、これは合理的に明らかです。 しかし、データベースへの接続も必要とする2つ目のテストケースを作成する場合はどうすればよいでしょう(メソッドTestDB2_1、TestDB2_2などでTTestDB2を呼び出しましょう)。
TTestSetup.Create
のコンストラクターは次のとおりです。
constructor TTestSetup.Create(ATest: ITest; AName: string = '');
つまり、スイートに「含める」ことができるテストケースは1つだけです。 このように書くと:
RegisterTest(TTestDBSetup.Create(TTestDB1.Suite)); RegisterTest(TTestDBSetup.Create(TTestDB2.Suite));
次に、スキームに従って呼び出しを受信します。
-- TTestDBSetup.SetUp ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_1 ---- TTestDB1.TearDown ---- TTestDB1.SetUp ------ TTestDB1.TestDB1_2 ---- TTestDB1.TearDown -- TTestDBSetup.TearDown -- TTestDBSetup.SetUp ---- TTestDB2.SetUp ------ TTestDB2.TestDB2_1 ---- TTestDB2.TearDown ---- TTestDB2.SetUp ------ TTestDB2.TestDB2_2 ---- TTestDB2.TearDown -- TTestDBSetup.TearDown

これは私たちが望むものではありません。 データベースに一度だけ接続したい。
実際、ここから始まり、この記事を書くきっかけとなりました。 RegisterTestメソッドの2番目のバリアントに注目しましょう。
procedure RegisterTest(SuitePath: string; test: ITest); begin assert(assigned(test)); if __TestRegistry = nil then CreateRegistry; RegisterTestInSuite(__TestRegistry, SuitePath, test); end;
SuitePath
とは
SuitePath
ですか?
RegisterTestInSuite
参照してください。
非表示のテキスト
procedure RegisterTestInSuite(rootSuite: ITestSuite; path: string; test: ITest); ... begin if (path = '') then begin // End any recursion rootSuite.addTest(test); end else begin // Split the path on the dot (.) dotPos := Pos('.', Path); if (dotPos <= 0) then dotPos := Pos('\', Path); if (dotPos <= 0) then dotPos := Pos('/', Path); if (dotPos > 0) then begin suiteName := Copy(path, 1, dotPos - 1); pathRemainder := Copy(path, dotPos + 1, length(path) - dotPos); end else begin suiteName := path; pathRemainder := ''; end; ...
また、SuitePathは部分に分割されており、これらの部分の区切りはピリオド、つまり これは、登録済みのテストケースが追加される一種の「パススイート」です。
次のようにTestDB2を登録しようとします(TTestDBSetupで「子ノード」としてTTestDB2を追加します)。
RegisterTest('Setup decorator ((d) TTestDB1)', TTestDB2.Suite);
うまくいきませんでした:

RegisterTestInSuite
コードをもう一度見てみましょう。
非表示のテキスト
procedure RegisterTestInSuite(rootSuite: ITestSuite; path: string; test: ITest); ... begin ... currentTest.queryInterface(ITestSuite, suite); if Assigned(suite) then begin ...
テストケースがITestSuiteに追加され、TTestSetupがこのインターフェイスを実装していないことがわかります。 どうする?
ここでは、たとえばIndySoapライブラリ(グループ化されたdUnitテストがあります)を覗いて、次のことを確認します(テストに関してすぐに記述します)。
... function DBSuite: ITestSuite; begin Result := TTestSuite.Create('DB tests'); Result.AddTest(TTestDB1.Suite); Result.AddTest(TTestDB2.Suite); end; ... initialization RegisterTest(TTestDBSetup.Create(DBSuite));
つまり、テストケースからスイートを作成し、このスイートをTTestSetupに追加します。

そして、すべてが機能しているようで、すべてが正常です。 これを行うことができます。
ただし、(より正確には「いつ」)データベーステストを追加する場合(TTestDB3と呼びましょう)、DBSuiteに追加する必要があります。
... function DBSuite: ITestSuite; begin ... Result.AddTest(TTestDB3.Suite); end; ...
さらに、適切な方法で、それらは別のモジュールで取り出す必要があり、このモジュールはDBSuite機能を使用してモジュールに既に追加されている必要があります。 個人的には、このDBSuiteの変更があまり好きではありません(テスト階層に視覚的に「冗長な」DBテストノードが追加されますが、TTestDB1 / TTestDB2はすぐにTTestDBSetupに「属する」ことができます)。 プロジェクトにテストモジュールを追加するだけで、テストモジュールは「自動的に」TTestDBSetupに追加されます。
まあ、私たちは望むようにやります。 まず、「Setup decorator((d)...」という形式のセットアップの名前が好きではありません。さらに、後でこのセットアップで他のテストを登録するときに、この名前を使用します。次のことに注意してください。
function TTestSetup.GetName: string; begin Result := Format(sSetupDecorator, [inherited GetName]); end;
そして、
AName
パラメーターで
constructor TTestSetup.Create(ATest: ITest; AName: string = '');
最終的に割り当てられる
constructor TAbstractTest.Create(AName: string); ... FTestName := AName; ...
再定義すると
... TTestDBSetup = ... public function GetName: string; override; ... implementation ... function TTestDBSetup.GetName: string; begin Result := FTestName; end; ... initialization RegisterTest(TTestDBSetup.Create(DBSuite, 'DB'));
それから私達は得る:

ここで、モジュールがプロジェクトに接続されたらすぐにテストケースを登録したい つまり、このように:
unit uTestDB3; ... initialization RegisterTest('DB', TTestDB3.Suite));
これを行うには、TTestDBSetupでITestSuiteインターフェイスを実装する必要があります(
RegisterTestInSuite
思い出してください)。
... ITestSuite = interface(ITest) ['{C20E38EF-7369-44D9-9D84-08E84EC1DCF0}'] procedure AddTest(test: ITest); procedure AddSuite(suite : ITestSuite); end;
次の2つの方法があります。
... TTestDBSetup = class(TTestSetup, ITestSuite) public procedure AddTest(test: ITest); procedure AddSuite(suite : ITestSuite); end; ... implementation ... procedure TTestDBSetup.AddTest(test: ITest); begin Assert(Assigned(test)); FTests.Add(test); end; procedure TTestDBSetup.AddSuite(suite: ITestSuite); begin AddTest(suite); end; ...

わかった!
ただし、起動時(F9、ところで)、TTestDB3テストは実行されないことがわかりました。

理由を理解するには、実装を見てください。
procedure TTestDecorator.RunTest(ATestResult: TTestResult); begin FTest.RunWithFixture(ATestResult); end;
つまり テストは、TTestDBSetupの作成時に指定されたもの(
FTest
)のみを実行します。
非表示のテキスト
constructor TTestDecorator.Create(ATest: ITest; AName: string); begin ... FTest := ATest; FTests:= TInterfaceList.Create; FTests.Add(FTest); end;
そして、後で追加したもの(
FTests
)-いいえ。 RunTestをオーバーライドしてそれらも実行します。
... TTestDBSetup = ... protected procedure RunTest(ATestResult: TTestResult); override; ... end. ... procedure TTestDBSetup.RunTest(ATestResult: TTestResult); var i: Integer; begin inherited; // , .. FTest for i := 1 to FTests.Count - 1 do (FTests[i] as ITest).RunWithFixture(ATestResult); end;
以下を開始します。

今では、すべてが大丈夫だと思われます。 ただし、よく見ると、統計ではテストの数が4で、起動されていることがわかります。6。明らかに、追加されたテストは考慮されていません。 混乱。
美しさをもたらしましょう:
非表示のテキスト
ここで、CountEnabledTestCasesとCountEnabledTestInterfacesはヘルパー関数です。
ノタベネ。 GUIバージョンはCountEnabledTestCasesをカウントし、コンソールはcountTestCasesをカウントします。
... TTestDBSetup = ... protected ... function CountTestInterfaces: Integer; function CountEnabledTestInterfaces: Integer; public ... function CountTestCases: Integer; override; function CountEnabledTestCases: Integer; override; end; ... function TTestDBSetup.CountTestCases: Integer; begin Result := inherited; if Enabled then Inc(Result, CountTestsInterfaces); end; function TTestDBSetup.CountTestInterfaces: Integer; var i: Integer; begin Result := 0; // skip FIRST test case (it is FTest) for i := 1 to FTests.Count - 1 do Inc(Result, (FTests[i] as ITest).CountTestCases); end; function TTestDBSetup.CountEnabledTestCases: Integer; begin Result := inherited; if Enabled then Inc(Result, CountEnabledTestInterfaces); end; function TTestDBSetup.CountEnabledTestInterfaces: Integer; var i: Integer; begin Result := 0; // skip FIRST test case (it is FTest) for i := 1 to FTests.Count - 1 do if (FTests[i] as ITest).Enabled then Inc(Result, (FTests[i] as ITest).CountTestCases); end; ...
ここで、CountEnabledTestCasesとCountEnabledTestInterfacesはヘルパー関数です。
ノタベネ。 GUIバージョンはCountEnabledTestCasesをカウントし、コンソールはcountTestCasesをカウントします。


今注文。
最後まで読んだ読者は尋ねるかもしれませんが、上記のDBSuiteのような関数を使用する代わりに気にする価値はありますか? 私自身は今それについて考えました。 しかし、私にとって、このソリューションの利点の1つは、プロジェクトの1つを作り直したことです。このプロジェクトでは、dUnitを理解する前でさえ、少し違ったやり方をしました。 そして、そのような可愛さをもたらすためには、1組のメソッドのみを修正する必要があります(基本クラスに上記を追加します)。
PS:ソースコードの例-github.com/ashumkin/habr-dunit-ttestsetup-demo
更新しました。 結果のクラス
TTestDBSetup
のソースコード(
TTestDBSetup
名前が変更され
TTestSetupEx
)は、別のdUnitExプロジェクトに移動されました( TestSetupEx.pasを参照)