dUnitでデータベーステストを整理する方法

ご存じのとおり、xUnitフレームワークでは、最も単純なテストケースはSetUp、TestSomething、TearDownの一連の呼び出しで構成されています。 ユニットテストでは、メインテストの前にいくつかのリソースを準備する必要が非常に頻繁にあります。 この典型的な例は、データベース接続です。 そして、ロジックは、それぞれの前にいくつかのテストを実行し、SetUpでデータベースへの接続を確立し、TearDownで切断することにより、非常にコストがかかることを示しています。



モジュールの例
... 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。明らかに、追加されたテストは考慮されていません。 混乱。



美しさをもたらしましょう:



非表示のテキスト
 ... 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を参照)



All Articles