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を参照)

Source: https://habr.com/ru/post/J266487/


All Articles