モバイルDelphiアプリケーションのデータベースのテスト

前の資料「モバイルDelphiアプリケーション用のDBMSの選択」では 、その名前が示すように、そのデータの保存とほとんどの処理を担当するアプリケーションサブシステムの開発の第1段階が示されました。 「ほとんど」についての明確化が理由で行われました。最終的には、ストアドプロシージャ(CP)を使用する機能のために指定された選択肢がInterbase DBMSに落ちたためです。それらを呼び出すタスク。



この特定のケースでのテストの必要性をよりよく理解するために、説明されたプロジェクトでは、かなり高い品質レベルが最初に設定され、特に主要なCPをチェックするセルフテストにより、手順で実装された機能の面でのメンテナンスが達成されたことに注意するアプリケーションの重要な機能を担当します- 推奨システム )。 このようなテストを編成する方法の1つ( DUnitXとXMLに基づく)は、この記事の主題です。



何か良いものはありましたか?



あなた自身がすでに適用している場合、または自動テストの利点を説明している文献に精通している場合、この半水セクションは、特に理論を含まないためスキップできますが、この特定のプロジェクトにもたらされる利点の例を示します。



忘れられた中断のために常に空の結果のようなプリミティブで愚かな簡単に検出可能なエラー、つまり、最小限のテストデータに記述されたコードの義務的な手動テストですぐに現れるすべてのエラーを破棄すると、問題が解決された2つのカテゴリを区別できます:



  1. 基本的に同様のテストが記述されているため、最も明らかなものは、必要なアルゴリズムが何らかの方法で誤って実装されている場合の論理エラーの層です 。 自動テストでは、このような欠陥を最大12個特定することができました。これらのケースの手動検査は、特別な特別に選択されたテストデータが必要なため、ほとんど見つかりませんでした。
  2. 歩留まりはそれほど豊かではありませんが、許容できないエラーにつながるのは、 Interbase自体の欠陥です 。 テストのおかげで、 不安定なカーソルHPへの接続などの微妙なニュアンスに不慣れなことが起こりました 。そのパラメーターはクエリテーブルから取得されます。


自動テストの楽しさを知った経験の浅い開発者は、どこでもどこでもそれを使用したいと思うかもしれません。しかし、世界平和は確かに素晴らしいですが、そのような願望の価格はいくらですか? 著者自身がテストに費やしました-テスト計画の作成、テストデータの選択、約3〜4週間(そして、以下で説明するように、起動のためのインフラストラクチャを作成する時間は考慮されません)。 したがって、それぞれのケースで利点とコストを個別に相関させることをお勧めします。



言葉から行為へ



自動テストが満たす必要のある技術要件を事前に定式化して、実際の部分に移りましょう



  1. クロスプラットフォーム :Windowsおよびモバイルデバイスで起動します。 デスクトップOSは非常に明白な理由で必要です。テストは何倍も高速に実行されるため、テストが開発され、その後モバイルOSでの最終実行が進行中です。
  2. DUnitXライブラリの使用-DUnitとは異なり、着実に開発されており、FireMonkeyには既製のグラフィカルインターフェイスがあります(サイズは中程度ですが)。すべてのプラットフォームで同じです。
  3. テストの実際のセマンティックコンテンツ(テストデータ、実行パラメーターを含むXPのリスト、期待される結果)からの起動インフラストラクチャ(DUnitX部分)の分離は、すべてXMLファイルに保存する必要があります 。 この要件によりタスクは多少複雑になりますが、利点もあります:テストの本質は整頓されたままであり、サービスDelphiコードのみがそれらを複雑にしません。 そのようなXMLの小さな断片は、言われたことをよりよく理解するのに役立ちます(それへの参照は、本文の後半にあります)。



    <?xml version="1.0" encoding="utf-8"?> <> <__> <_> < ="SHOPPING_LIST"/> </_> </__> <__/> <> <!--    .--> <>...</> ... <!--2 ,   .--> <> <_> < ="SHOPPING_LIST"> <> <ID ="">1</ID> <NAME =""> </NAME> <ADD_DATE ="__">1.2.2015</ADD_DATE> </> </> < ="LIST_ITEM"> <> <ID ="">1</ID> <LIST_ID ="">1</LIST_ID> <GOODS_ID ="">107</GOODS_ID> <AMOUNT ="">1</AMOUNT> <ADD_DATE ="__">25.2.2015 15:12</ADD_DATE> </> ... </> </_> < ="RECOMMEND_GOODS_TO_EMPTY_LIST" _=""> <> <_> <TARGET_DATE ="__">16.9.2014</TARGET_DATE> </_> </> ... <> <_> <TARGET_DATE ="__">1.3.2015</TARGET_DATE> </_> <> <> <GOODS_ID ="">107</GOODS_ID> <RECOMMENDATION_ID ="">0</RECOMMENDATION_ID> <ACCURACY ="">0.75</ACCURACY> </> </> </> ... </> </> <>...</> ... </> </>
          
          





実際のファイルは、この例よりもはるかに大きく(3,000行から4,500行まで)、テキスト形式で編集するのは合理的ではないようです-テストで既にミスをする可能性が非常に高くなるため、XMLを操作するテーブルモードを備えた専用のAltova XMLSpyエディターが使用されました。 このツールのおかげで、タグやネストなどに気を取られることなく、本質に完全に集中できます。









DUnitX拡張



選択したライブラリを使用すると、任意のクラスにテストを配置し、次のような特別な属性を使用して、それに対応するメソッドをマークできます。



 [TestFixture] TTestSet = class public [SetupFixture] procedure Setup; [TearDownFixture] procedure Teardown; [Setup] procedure TestSetup; [TearDown] procedure TestTeardown; [Test] procedure Test1; [Test] [TestCase(' 1', '1,1')] [TestCase(' 2', '2,2')] procedure Test2(const IntegerParameter: Integer; const StringParameter: string); end;
      
      





ここで、 TTestSet



は2つのテストのテストセット(ライブラリに関してはフィクスチャ)であり、2番目のテストはこれらのオプションの両方で数回実行されます。 ただし、このような標準的な方法は、コンパイル段階でテストとパラメーター値の数が静的に設定され、テストセットのリスト(XMLファイルごとに1つ)と各テストのリストの両方を動的形成する必要があるため、完全に適していませんそれら(「Test」タグによって対応するファイルから取得)。



出現した障害は、プラグインメカニズムにより簡単に克服できます。DUnitXを使用すると、セットを柔軟に作成し、必要に応じてセットを埋めることができます。 さらに、属性に基づく「ボックス化」メカニズムもプラグインとして実装されています( DUnitX.FixtureProviderPlugin.pasを参照)。これは、十分にテストされており、作業する際に驚くことはないことを意味します。



モジュールの「インターフェース」部分



結果のクラスからの新しいプラグインでモジュールの検討を開始し、メソッドの実装は少し後にします。



 unit Tests.XMLFixtureProviderPlugin; interface implementation uses DUnitX.TestFramework, DUnitX.Utils, DUnitX.Extensibility, ... Xml.XMLDoc, {$IFDEF MSWINDOWS} Xml.Win.msxmldom {$ELSE} Xml.omnixmldom {$ENDIF}; type TXMLFixtureProviderPlugin = class(TInterfacedObject, IPlugin) protected procedure GetPluginFeatures(const context: IPluginLoadContext); end; TXMLFixtureProvider = class(TInterfacedObject, IFixtureProvider) protected procedure GenerateTests(const Fixture: ITestFixture; const FileName: string); procedure Execute(const context: IFixtureProviderContext); end; TDBTests = class abstract {    ,     ,   ,      «»,  ,        . } ... public procedure Setup; virtual; procedure Teardown; virtual; procedure TestSetup; virtual; abstract; procedure TestTeardown; virtual; abstract; procedure Test(const TestIndex: Integer); virtual; abstract; end; TXMLBasedDBTests = class(TDBTests) private const TestsTag = ''; ... private FFileName: string; FXML: TXMLDocument; //    ,     XML. ... public procedure AfterConstruction; override; {      AfterConstruction –   Setup     .     : https://github.com/VSoftTechnologies/DUnitX/commit/267111f4feec77d51bf2307a194f44106d499680#diff-745fb4ee38a43631f57d1b6ef88e0ffcR212 } destructor Destroy; override; [SetupFixture] procedure Setup; override; [TearDownFixture] procedure Teardown; override; [Setup] procedure TestSetup; override; [TearDown] procedure TestTeardown; override; [Test] procedure Test(const TestIndex: Integer); override; function DetermineTestIndexes: TArray<Integer>; property FileName: string read FFileName write FFileName; end; //     ... initialization TDUnitX.RegisterPlugin(TXMLFixtureProviderPlugin.Create); end.
      
      





最初の2つのクラスTXMLFixtureProviderPlugin



およびTXMLFixtureProvider



、プラグインの既存のシステムに埋め込むために必要であり、それらのメソッドの実装によってのみ興味深いものです。 次のTDBTests



もあまり関心がありません。これはTDBTests



、DB固有のものをカプセル化する目的で階層に割り当てられるため、相続人TXMLBasedDBTests



直接アクセスする必要があるためです。 彼の責任は彼の人生の段階に関連しています。





メソッドの実装



クラスの目的を理解した後は、残りのコードのデモを行います。



  1. TXMLFixtureProviderPlugin







     procedure TXMLFixtureProviderPlugin.GetPluginFeatures(const context: IPluginLoadContext); begin context.RegisterFixtureProvider(TXMLFixtureProvider.Create); end;
          
          



  2. TXMLFixtureProvider







     procedure TXMLFixtureProvider.Execute(const context: IFixtureProviderContext); var XMLDirectory, XMLFile: string; begin {$IFDEF MSWINDOWS} XMLDirectory := {    .}; {$ELSE} XMLDirectory := TPath.GetDocumentsPath; {$ENDIF} for XMLFile in TDirectory.GetFiles(XMLDirectory, '*.xml') do GenerateTests ( context.CreateFixture(TXMLBasedDBTests, TPath.GetFileNameWithoutExtension(XMLFile), ''), XMLFile ); end; procedure TXMLFixtureProvider.GenerateTests(const Fixture: ITestFixture; const FileName: string); procedure FillSetupAndTeardownMethods(const RTTIMethod: TRttiMethod); var Method: TMethod; TestMethod: TTestMethod; begin Method.Data := Fixture.FixtureInstance; Method.Code := RTTIMethod.CodeAddress; TestMethod := TTestMethod(Method); if RTTIMethod.HasAttributeOfType<SetupFixtureAttribute> then Fixture.SetSetupFixtureMethod(RTTIMethod.Name, TestMethod); if RTTIMethod.HasAttributeOfType<TearDownFixtureAttribute> then Fixture.SetTearDownFixtureMethod(RTTIMethod.Name, TestMethod, RTTIMethod.IsDestructor); if RTTIMethod.HasAttributeOfType<SetupAttribute> then Fixture.SetSetupTestMethod(RTTIMethod.Name, TestMethod); if RTTIMethod.HasAttributeOfType<TearDownAttribute> then Fixture.SetTearDownTestMethod(RTTIMethod.Name, TestMethod); end; var XMLTests: TXMLBasedDBTests; RTTIContext: TRttiContext; RTTIMethod: TRttiMethod; TestIndex: Integer; begin XMLTests := Fixture.FixtureInstance as TXMLBasedDBTests; XMLTests.FileName := FileName; RTTIContext := TRttiContext.Create; try for RTTIMethod in RTTIContext.GetType(Fixture.TestClass).GetMethods do begin FillSetupAndTeardownMethods(RTTIMethod); if RTTIMethod.HasAttributeOfType<TestAttribute> then for TestIndex in XMLTests.DetermineTestIndexes do Fixture.AddTestCase( RTTIMethod.Name, TestIndex.ToString, '', '', RTTIMethod, True, [TestIndex] ); end; finally RTTIContext.Free; end; end;
          
          



  3. TDBTests







     procedure TDBTests.Setup; begin //         . ... end; procedure TDBTests.Teardown; begin //   . ... end;
          
          



  4. TXMLBasedDBTests







     procedure TXMLBasedDBTests.AfterConstruction; begin inherited; //  FXML. ... FXML.DOMVendor := GetDOMVendor({$IFDEF MSWINDOWS} SMSXML {$ELSE} sOmniXmlVendor {$ENDIF}); //  . ... end; destructor TXMLBasedDBTests.Destroy; begin //  . ... inherited; end; function TXMLBasedDBTests.DetermineTestIndexes: TArray<Integer>; var TestsNode: IXMLNode; TestIndex: Integer; TestIndexList: TList<Integer>; begin FXML.LoadFromFile(FFileName); try TestsNode := FXML.DocumentElement.ChildNodes[TestsTag]; TestIndexList := TList<Integer>.Create; try for TestIndex := 0 to TestsNode.ChildNodes.Count - 1 do if {       ?} then TestIndexList.Add(TestIndex); Result := TestIndexList.ToArray; finally TestIndexList.Free; end; finally FXML.Active := False; end; end; procedure TXMLBasedDBTests.Setup; begin inherited; FXML.LoadFromFile(FFileName); end; procedure TXMLBasedDBTests.Teardown; begin FXML.Active := False; inherited; end; procedure TXMLBasedDBTests.TestSetup; begin inherited; //     «__». ... end; procedure TXMLBasedDBTests.TestTeardown; begin inherited; //     «__». ... end; procedure TXMLBasedDBTests.Test(const TestIndex: Integer); var TestNode: IXMLNode; begin inherited; TestNode := FXML.DocumentElement.ChildNodes[TestsTag].ChildNodes[TestIndex]; //   . ... end;
          
          





GUI



自動テストの要件には、起動を制御してその結果を表示できる既製のフォームが記載されていました。これはDUNitX.Loggers.MobileGUI.pasに関するもので、わずかな外観上の改善が適用されました。 意図的に1つのテストに失敗した3つのプラットフォームでの実行結果を以下に示します。



  1. Windows(実行時間7秒)



    テストリスト(Windows)テスト実行結果(Windows)
  2. Android(実行時間35秒)



    テストリスト(Android)起動結果のテスト(Android)
  3. iOS(実行時間28秒)



    テストリスト(iOS)起動結果のテスト(iOS)



All Articles