📜 ⬆️ ⬇️

MindStream. How do we write software under FireMonkey. Part 3. DUnit + FireMonkey

Part 1 .
Part 2 .

Hello.

In this article I want to introduce readers to the process of transferring VCL code to FireMonkey. In the standard Delphi package, starting in my opinion from version 2009, the DUnit project comes out of the box.
')
However, it was written in the distant past VCL. And although it allows testing the code written for FireMonkey (thanks to the console output), it doesn’t have the “nagging” GUIRunner that many of us are used to, because it’s very quick and easy to remove those tests that we don’t want run "right now."

image




For those who are completely or little acquainted with DUnit. In normal mode out of the box, the documentation suggests making File-> New-> Other-> Unit Test-> TestProject. Next, you need to choose a GUI or console option. Thanks to these not so difficult manipulations, you have a new project that should look something like this (at least “my” XE7, this is the code that was generated) for the GUI:

program Project1Tests; { Delphi DUnit Test Project ------------------------- This project contains the DUnit test framework and the GUI/Console test runners. Add "CONSOLE_TESTRUNNER" to the conditional defines entry in the project options to use the console test runner. Otherwise the GUI test runner will be used by default. } {$IFDEF CONSOLE_TESTRUNNER} {$APPTYPE CONSOLE} {$ENDIF} uses DUnitTestRunner, TestUnit1 in 'TestUnit1.pas', Unit1 in '..\DUnit.VCL\Unit1.pas'; {$R *.RES} begin DUnitTestRunner.RunRegisteredTests; end. 

Next we add TestCase, this is also done (File-> New-> Other-> Unit Test-> TestCase), as a result there should be something similar:

 unit TestUnit1; { Delphi DUnit Test Case ---------------------- This unit contains a skeleton test case class generated by the Test Case Wizard. Modify the generated code to correctly setup and call the methods from the unit being tested. } interface uses TestFramework, System.SysUtils, Vcl.Graphics, Winapi.Windows, System.Variants, System.Classes, Vcl.Dialogs, Vcl.Controls, Vcl.Forms, Winapi.Messages, Unit1; type // Test methods for class TForm1 TestTForm1 = class(TTestCase) strict private FForm1: TForm1; public procedure SetUp; override; procedure TearDown; override; published procedure TestDoIt; end; implementation procedure TestTForm1.SetUp; begin FForm1 := TForm1.Create; end; procedure TestTForm1.TearDown; begin FForm1.Free; FForm1 := nil; end; procedure TestTForm1.TestDoIt; var ReturnValue: Integer; begin ReturnValue := FForm1.DoIt; // TODO: Validate method results end; initialization // Register any test cases with the test runner RegisterTest(TestTForm1.Suite); end. 

In general, my example shows how easy it is to add testing, even for Delphi7. All we need is to call DUnitTestRunner.RunRegisteredTests ;. And add new files with TestCase to the project. In more detail the question of testing using DUnit is considered here .

To implement, I decided that you just need to repeat the guys who did DUnit.
The first problem (The fact that TTreeNode, and TTreeViewItem “are not brothers at all” will not even speak, the documentation will save everyone) that I encountered was here:

 type TfmGUITestRunner = class(TForm) ... protected FSuite: ITest; procedure SetSuite(Value: ITest); ... public property Suite: ITest read FSuite write SetSuite; end; procedure RunTestModeless(aTest: ITest); var l_GUI: TfmGUITestRunner; begin Application.CreateForm(TfmGUITestRunner, l_GUI); l_GUI.Suite := aTest; l_GUI.Show; end; procedure TfmGUITestRunner.SetSuite(Value: ITest); begin FSuite := Value; // AV  if FSuite <> nil then InitTree; end; 

The problem, as always, is “recognized” in the debug, well, or in the documentation :). In FireMonkey, Application.CreateForm () ;, does not create a form. Yes, strangely enough. TApplication.CreateForm

My comment to the commit when I figured out :)
FSuite; It has not yet been created, because Application.CreateForm is in fact, if it is not kicked explicitly, “it does not create a bitch of normal forms, but only references to future classes. What accordingly affects class members who are not at all nil, as they should be? ”

AV will come out in System._IntfCopy (var Dest: IInterface; const Source: IInterface);
And it will come out because in Dest we will have garbage, not interface or nil. And this will manifest when we at the previous interface (if it is not // nil) will subtract 1.

Even if we prescribe such a line,
FSuite: = nil;


Here is another link on this issue -. It doesn’t say what it does! To be honest, I, too, was a bit shocked that the method called Make a Form does not make it.
We solve the problem by creating forms explicitly (l_GUI: = TfmGUITestRunner.create (nil);) and go on.

Now we need to build a test tree based on the TestCase'of added for testing. If you noticed, the form building process begins with the RunRegisteredTestsModeless method:

 procedure RunRegisteredTestsModeless; begin RunTestModeless(registeredTests) end; 

I decided not to put this method into a separate module, as the creators of DUnit, so to connect fmGUITestRunner, you need to specify the module in the project code, well, actually call the desired method. In my case, the project code looks like this:

 program FMX.DUnit; uses FMX.Forms, //   u_fmGUITestRunner in 'u_fmGUITestRunner.pas' {fmGUITestRunner}, //  u_FirstTest in 'u_FirstTest.pas', u_TCounter in 'u_TCounter.pas', u_SecondTest in 'u_SecondTest.pas'; {$R *.res} begin Application.Initialize; //      u_fmGUITestRunner.RunRegisteredTestsModeless; Application.Run; end. 

The attentive reader will notice that we have not added any registeredTests, and have never indicated at all what tests we will have added. RegisteredTests is the “global” TestFrameWork method, which is connected to our form, it returns a global variable - __TestRegistry: ITestSuite;

The way TestCases “fall” into this variable will be left outside the scope of this article, especially since the creators of DUnit have done the work. However, if readers express an interest in this topic, I will answer in comments. So back to the tree. Method to initialize the tree:

 procedure TfmGUITestRunner.InitTree; begin FTests.Clear; FillTestTree(Suite); TestTree.ExpandAll; end; 

FTests is a list of interface objects that will store the list of our tests. In turn, the FillTestTree method is overloaded, this is done because we do not know, we are working with the root element of the tree, or with a regular node:

 ... procedure FillTestTree(aTest: ITest); overload; procedure FillTestTree(aRootNode: TTreeViewItem; aTest: ITest); overload; ... procedure TfmGUITestRunner.FillTestTree(aRootNode: TTreeViewItem; aTest: ITest); var l_TestTests: IInterfaceList; l_Index: Integer; l_TreeViewItem: TTreeViewItem; begin if aTest = nil then Exit; l_TreeViewItem := TTreeViewItem.Create(self); l_TreeViewItem.IsChecked := True; //    ,    Tag   .        :) l_TreeViewItem.Tag := FTests.Add(aTest); l_TreeViewItem.Text := aTest.Name; //   ,   if aRootNode = nil then TestTree.AddObject(l_TreeViewItem) else aRootNode.AddObject(l_TreeViewItem); // ITest,   Tests,   (IInterfaceList) ""  //      l_TestTests := aTest.Tests; for l_Index := 0 to l_TestTests.Count - 1 do FillTestTree(l_TreeViewItem, l_TestTests[l_Index] as ITest); end; 

As we see, in the method we not only filled the tree, but also gave information to each node which of the tests corresponds to it. In order to get a test by node, write the NodeToTest method:

 function TfmGUITestRunner.NodeToTest(aNode: TTreeViewItem): ITest; var l_Index: Integer; begin assert(aNode.Tag >= 0); l_Index := aNode.Tag; Result := FTests[l_Index] as ITest; end; 

Now add the "knowledge" of the tests. Each test has a GUIObject variable, of type TObject. SetupGUINodes we will call on FormShow.

 procedure TfmGUITestRunner.SetupGUINodes(aNode: TTreeViewItem); var l_Test: ITest; l_Index: Integer; begin for l_Index := 0 to Pred(aNode.Count) do begin //   l_Test := NodeToTest(aNode.Items[l_Index]); assert(assigned(l_Test)); //      l_Test.GUIObject := aNode.Items[l_Index]; SetupGUINodes(aNode.Items[l_Index]); end; end; 

In order to get a node from the test, we write the method:

 function TfmGUITestRunner.TestToNode(test: ITest): TTreeViewItem; begin assert(assigned(test)); Result := test.GUIObject as TTreeViewItem; assert(assigned(Result)); end; 

The way I “connected” the tests with the tree, I, and the older colleague did not like it. Why did the DUnit developers go this way, I guess. DUnit was written a long time ago, and there were no Generics at that time. In the future, of course, we will redo it. At the end of the article I will write about our next revisions and "Wishlist".

So - our tree is being built, tests are in FTests. Tests and wood are related. It's time to run the tests, and process the results. In order for the form to do this, we add the implementation of the ITestListener interface described in TestFrameWork:

  { ITestListeners get notified of testing events. See TTestResult.AddListener() } ITestListener = interface(IStatusListener) ['{114185BC-B36B-4C68-BDAB-273DBD450F72}'] procedure TestingStarts; procedure StartTest(test: ITest); procedure AddSuccess(test: ITest); procedure AddError(error: TTestFailure); procedure AddFailure(Failure: TTestFailure); procedure EndTest(test: ITest); procedure TestingEnds(testResult :TTestResult); function ShouldRunTest(test :ITest):Boolean; end; 

Add these methods to the class description, and implement them:

 procedure TfmGUITestRunner.TestingStarts; begin FTotalTime := 0; end; procedure TfmGUITestRunner.StartTest(aTest: ITest); var l_Node: TTreeViewItem; begin assert(assigned(TestResult)); assert(assigned(aTest)); l_Node := TestToNode(aTest); assert(assigned(l_Node)); end; procedure TfmGUITestRunner.AddSuccess(aTest: ITest); begin assert(assigned(aTest)); SetTreeNodeFont(TestToNode(aTest), c_ColorOk) end; procedure TfmGUITestRunner.AddError(aFailure: TTestFailure); var l_ListViewItem: TListViewItem; begin SetTreeNodeFont(TestToNode(aFailure.failedTest), c_ColorError); l_ListViewItem := AddFailureNode(aFailure); end; procedure TfmGUITestRunner.AddFailure(aFailure: TTestFailure); var l_ListViewItem: TListViewItem; begin SetTreeNodeFont(TestToNode(aFailure.failedTest), c_ColorFailure); l_ListViewItem := AddFailureNode(aFailure); end; procedure TfmGUITestRunner.EndTest(test: ITest); begin // ,          // .     . //    ,     ,    //  ,  TODO // assert(False); end; procedure TfmGUITestRunner.TestingEnds(aTestResult: TTestResult); begin FTotalTime := aTestResult.TotalTime; end; function TfmGUITestRunner.ShouldRunTest(aTest: ITest): Boolean; var l_Test: ITest; begin //  ,    .    ""  ""   l_Test := aTest; Result := l_Test.Enabled end; 

There is nothing special to explain here. Although if you have questions, I will answer in detail. In the original DUnitRunner, when “receiving” the test result, I changed the picture at the corresponding tree node. I decided not to fool around with pictures, because now I don’t have them out of the box, and adding a picture to a node is somehow confused through styles. Therefore, I decided to limit myself to changing FontColor and FontStyle for each node.

Like a business for 1 minute, and spent a couple of hours digging through all the documentation:

 procedure TfmGUITestRunner.SetTreeNodeFont(aNode: TTreeViewItem; aColor: TAlphaColor); begin //          ,     aNode.StyledSettings := aNode.StyledSettings - [TStyledSetting.ssFontColor, TStyledSetting.ssStyle]; aNode.Font.Style := [TFontStyle.fsBold]; aNode.FontColor := aColor; end; 

To display the results we will use ListView. The features of TListView in FireMonkey are such that the list is completely sharpened for mobile applications. And lost the wonderful property of the Columns. To add errors, add the AddFailureNode method:

 function TfmGUITestRunner.AddFailureNode(aFailure: TTestFailure): TListViewItem; var l_Item: TListViewItem; l_Node: TTreeViewItem; begin assert(assigned(aFailure)); l_Item := lvFailureListView.Items.Add; l_Item.Text := aFailure.failedTest.Name + '; ' + aFailure.thrownExceptionName + '; ' + aFailure.thrownExceptionMessage + '; ' + aFailure.LocationInfo + '; ' + aFailure.AddressInfo + '; ' + aFailure.StackTrace; l_Node := TestToNode(aFailure.failedTest); while l_Node <> nil do begin l_Node.Expand; l_Node := l_Node.ParentItem; end; Result := l_Item; end; 

It's time to run our tests, for which we will add a button and a launch method:

 procedure TfmGUITestRunner.btRunAllTestClick(Sender: TObject); begin if Suite = nil then Exit; ClearResult; RunTheTest(Suite); end; procedure TfmGUITestRunner.RunTheTest(aTest: ITest); begin TestResult := TTestResult.Create; try TestResult.addListener(self); aTest.run(TestResult); finally FreeAndNil(FTestResult); end; end; 

We launch our Runner, press the test launch button, as a result we see:

image


The last thing left for us to do is to process the user's actions during the change of the node state:

 procedure TfmGUITestRunner.TestTreeChangeCheck(Sender: TObject); begin SetNodeEnabled(Sender as TTreeViewItem, (Sender as TTreeViewItem).IsChecked); end; procedure TfmGUITestRunner.SetNodeEnabled(aNode: TTreeViewItem; aValue: Boolean); var l_Test: ITest; begin l_Test := NodeToTest(aNode); if l_Test <> nil then l_Test.Enabled := aValue; end; 

Change the state of the checkboxes of some nodes:

image


The test code on which I actually tested:

 unit u_SecondTest; interface uses TestFrameWork; type TSecondTest = class(TTestCase) published procedure DoIt; procedure OtherDoIt; procedure ErrorTest; procedure SecondErrorTest; end; // TFirstTest implementation procedure TSecondTest.DoIt; begin Check(true); end; procedure TSecondTest.ErrorTest; begin raise ExceptionClass.Create('Error Message'); end; procedure TSecondTest.OtherDoIt; begin Check(true); end; procedure TSecondTest.SecondErrorTest; begin Check(False); end; initialization TestFrameWork.RegisterTest(TSecondTest.Suite); end. 

To summarize - at this stage, we got quite a working application for testing FireMonkey code using the familiar GUIRunner. The project is open, so everyone can use it.

Future plans:
Write a tree traversal method that will receive lambda . The tree has to be bypassed constantly, but the actions with each branch are different, so lambda seems appropriate to me.

Comments and suggestions from my older colleague :
Reconnect Test Site Node at TDictionary <TTreeViewItem, ITest> docwiki.embarcadero.com/Libraries/XE7/en/System.Generics.Collections.TDictionary
Add a graphical indicator “pass tests”. Buttons - select all, remove all, etc. as well as the output of test results (run time, the number of successful and failures, etc.).
Add a pattern Decorator to get rid of the “crutch” GUIObject.

In the near future, we will begin to test our main project, MindStream, as well as by bringing Runner to our minds a little bit. Thanks to everyone who read to the end. Comments and criticisms, as always welcome in the comments.

Link to the repository.

ps The project is located in the repository MindStream \ FMX.DUnit

Links that I found, and which came in handy to me in the process:
sourceforge.net/p/radstudiodemos/code/HEAD/tree/branches/RadStudio_XE5_Update/FireMonkey/Delphi
fire-monkey.ru
18delphi.blogspot.ru
www.gunsmoker.ru
GUI testing "in Russian". Test Level Note
Once again about the "testing levels"
and of course
docwiki.embarcadero.com/RADStudio/XE7/en/Main_Page

Part 3.1

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


All Articles