
MindStream. As 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 porting VCL code to FireMonkey. In the standard Delphi package, starting in my opinion with version 2009, the DUnit project comes out of the box.
However, it was written in ancient times by VCL. And although it allows you to test code written for FireMonkey (thanks to console output), it doesn’t have a “nice” GUIRunner, which many of us are used to, because it’s very quick and easy to remove tests that we don’t want launch "right now."

For those who are very or little familiar with DUnit. In normal mode out of the box, the documentation suggests doing File-> New-> Other-> Unit Test-> TestProject. Next, you need to choose a GUI or console option. Thanks to these not so complicated manipulations, you have a new project that should look something like this (at least “mine” XE7, generated exactly this kind of code) for the GUI:
Next we add TestCase, this is also done (File-> New-> Other-> Unit Test-> TestCase), the result should be something similar:
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 discussed here .
For implementation, 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” I will not even say, the documentation will save everyone) that I encountered was here:
The problem, as always, is “recognized” in the debug, well, or in the documentation :). In FireMonkey, Application.CreateForm (); does not create a form. Yes, oddly enough. TApplication.CreateForm
Here is another link on this subject -. It doesn't do what it says it does! To be honest, I was also a little shocked by the fact that the method called Make Form does not do it.
We solve the problem by creating forms explicitly (l_GUI: = TfmGUITestRunner.create (nil);) and move on.
Now we need to build a test tree based on TestCase, which are added for testing. If you noticed, the process of building a form begins with the RunRegisteredTestsModeless method:
I decided not to put this method in a separate module, like the creators of DUnit, so to connect fmGUITestRunner, you need to specify the module in the project code, and actually call the desired method. In my case, the project code looks like this:
An attentive reader, pay attention that we did not add any registeredTests, and did not indicate at all which tests we would add. RegisteredTests is a "global" TestFrameWork method that is connected to our form; it returns a global variable - __TestRegistry: ITestSuite;
How TestCase "fall" into this variable, I will leave out of the scope of this article, especially since the creators of DUnit did the work. However, if readers express interest in this topic, I will answer in comments. So, back to the tree. Method for initializing the tree:
FTests, this is a list of interface objects that will store a list of our tests. In turn, the FillTestTree method is overloaded, this is done, since we do not know whether we are working with the root element of the tree, or with a normal node:
As you can 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 the test by node, we write the NodeToTest method:
Now add the “knowledge” to the tests. Each test has a GUIObject variable, such as TObject. We will call SetupGUINodes on FormShow.
In order to get a node from the test, we will write a method:
The way I “connected” the tests with the tree, I, and the senior colleague, did not like it. Why did DUnit developers go this way, I guess. DUnit was written for a long time, and then there were no Generics. In the future, we will certainly redo this. At the end of the article I will write about our next improvements and Wishlist.
So - our tree is under construction, tests are in FTests. Tests and tree are interconnected. It's time to run the tests, and process the results. In order for the form to do this, add the implementation of the ITestListener interface described in TestFrameWork:
Add these methods to the class description and implement them:
There is nothing special to explain. Although if there are questions, I will answer in detail. In the original, DUnitRunner, when "receiving" the test result, changed the picture of the corresponding tree node. I decided not to fool around with the pictures, because now I don’t have them out of the box, and adding the picture to the node is somehow messed up through the styles. Therefore, I decided to limit myself to changing FontColor and FontStyle for each node.
It seems like a business for 1 minute, but spent a couple of hours digging through all the documentation:
We will use ListView to display the results. Features of TListView in FireMonkey are such that the list is fully tailored for mobile applications. And lost the wonderful Columns property. To add errors, add the AddFailureNode method:
It's time to run our tests, for which we add a button and a launch method:
We start our Runner, click the test run button, as a result of which we see:

The last thing that remains for us to do is process the user’s actions when the state of the node changes:
Change the state of the checkboxes of some nodes:

The test code on which I actually tested:
To summarize - at this stage, we got a very 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 a lambda . The tree has to be bypassed constantly, but the actions with each branch are different, so the lambda seems appropriate to me.
Remarks and suggestions from my senior colleague :
Remake the Test Node link on TDictionarydocwiki.embarcadero.com/Libraries/XE7/en/System.Generics.Collections.TDictionary
Add a graphical indicator of “passing tests”. Buttons - select all, remove all, etc. as well as the conclusion of testing results (lead time, number of successful and failed, etc.).
Add a Decorator pattern to get rid of the “crutch” GUIObject.
In the near future, we will begin to test our main project, MindStream, and we will bring Runner to mind a little bit. Thanks to everyone who read to the end. Comments and criticism, as always welcome in the comments.
Link to the repository.
ps The project is located in the MindStream \ FMX.DUnit repository The
links that I found and which were useful 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". A note on test levels
Once again on “test levels”
and of course
docwiki.embarcadero.com/RADStudio/XE7/en/Main_Page
Part 3.1
Part 2 .
Hello.
In this article I want to introduce readers to the process of porting VCL code to FireMonkey. In the standard Delphi package, starting in my opinion with version 2009, the DUnit project comes out of the box.
However, it was written in ancient times by VCL. And although it allows you to test code written for FireMonkey (thanks to console output), it doesn’t have a “nice” GUIRunner, which many of us are used to, because it’s very quick and easy to remove tests that we don’t want launch "right now."

For those who are very or little familiar with DUnit. In normal mode out of the box, the documentation suggests doing File-> New-> Other-> Unit Test-> TestProject. Next, you need to choose a GUI or console option. Thanks to these not so complicated manipulations, you have a new project that should look something like this (at least “mine” XE7, generated exactly this kind of code) 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), the result 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 discussed here .
For implementation, 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” I will not even say, 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, oddly enough. TApplication.CreateForm
My comment on the commit when I figured out :)
FSuite, It’s not created yet, because Application.CreateForm actually, if you do not explicitly kick it, “does not create a bitch, normal forms, but only links to future classes. Which accordingly affects the members of the class who are not at all nil, as they should be. ”
AV will crawl out into System._IntfCopy (var Dest: IInterface; const Source: IInterface);
And it will come out because we have garbage in Dest, and not interface or nil. And this will manifest itself when we at the previous interface (if it is not // nil) will subtract 1.
Even if we write such a line, it’s up to
FSuite: = nil;
AV will crawl out into System._IntfCopy (var Dest: IInterface; const Source: IInterface);
And it will come out because we have garbage in Dest, and not interface or nil. And this will manifest itself when we at the previous interface (if it is not // nil) will subtract 1.
Even if we write such a line, it’s up to
FSuite: = nil;
Here is another link on this subject -. It doesn't do what it says it does! To be honest, I was also a little shocked by the fact that the method called Make Form does not do it.
We solve the problem by creating forms explicitly (l_GUI: = TfmGUITestRunner.create (nil);) and move on.
Now we need to build a test tree based on TestCase, which are added for testing. If you noticed, the process of building a form begins with the RunRegisteredTestsModeless method:
procedure RunRegisteredTestsModeless;
begin
RunTestModeless(registeredTests)
end;
I decided not to put this method in a separate module, like the creators of DUnit, so to connect fmGUITestRunner, you need to specify the module in the project code, and 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.
An attentive reader, pay attention that we did not add any registeredTests, and did not indicate at all which tests we would add. RegisteredTests is a "global" TestFrameWork method that is connected to our form; it returns a global variable - __TestRegistry: ITestSuite;
How TestCase "fall" into this variable, I will leave out of the scope of this article, especially since the creators of DUnit did the work. However, if readers express interest in this topic, I will answer in comments. So, back to the tree. Method for initializing the tree:
procedure TfmGUITestRunner.InitTree;
begin
FTests.Clear;
FillTestTree(Suite);
TestTree.ExpandAll;
end;
FTests, this is a list of interface objects that will store a list of our tests. In turn, the FillTestTree method is overloaded, this is done, since we do not know whether we are working with the root element of the tree, or with a normal 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 you can 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 the test by node, we 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” to the tests. Each test has a GUIObject variable, such as TObject. We will call SetupGUINodes 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 will write a 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 senior colleague, did not like it. Why did DUnit developers go this way, I guess. DUnit was written for a long time, and then there were no Generics. In the future, we will certainly redo this. At the end of the article I will write about our next improvements and Wishlist.
So - our tree is under construction, tests are in FTests. Tests and tree are interconnected. It's time to run the tests, and process the results. In order for the form to do this, 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. Although if there are questions, I will answer in detail. In the original, DUnitRunner, when "receiving" the test result, changed the picture of the corresponding tree node. I decided not to fool around with the pictures, because now I don’t have them out of the box, and adding the picture to the node is somehow messed up through the styles. Therefore, I decided to limit myself to changing FontColor and FontStyle for each node.
It seems like a business for 1 minute, but 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;
We will use ListView to display the results. Features of TListView in FireMonkey are such that the list is fully tailored for mobile applications. And lost the wonderful Columns property. 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 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 start our Runner, click the test run button, as a result of which we see:

The last thing that remains for us to do is process the user’s actions when the state of the node changes:
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:

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 a very 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 a lambda . The tree has to be bypassed constantly, but the actions with each branch are different, so the lambda seems appropriate to me.
Remarks and suggestions from my senior colleague :
Remake the Test Node link on TDictionary
Add a graphical indicator of “passing tests”. Buttons - select all, remove all, etc. as well as the conclusion of testing results (lead time, number of successful and failed, etc.).
Add a Decorator pattern to get rid of the “crutch” GUIObject.
In the near future, we will begin to test our main project, MindStream, and we will bring Runner to mind a little bit. Thanks to everyone who read to the end. Comments and criticism, as always welcome in the comments.
Link to the repository.
ps The project is located in the MindStream \ FMX.DUnit repository The
links that I found and which were useful 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". A note on test levels
Once again on “test levels”
and of course
docwiki.embarcadero.com/RADStudio/XE7/en/Main_Page
Part 3.1