Original in Russian: http://habrahabr.ru/post/248027/
Table of contents
Hello, dear readership.
In this post I want to tell about the changes in our project and also about the technologies and methods we’ve used to achieve our goals.
Now our project looks as follows:
Each shape is now capable of “being the diagram”. It means we can choose a shape and build a new diagram “inside”. It is clearly demonstrated below.
The object TmsPicker is responsible for the capability of “falling into”. The object TmsUpToParrent is responsible for the returning to the parent diagram.
We’ve also got ToolBar in which all shapes intended for drawing are drawn dynamically. It also realizes the capability of creating special shapes, for example, for the object of transfer (under the red square):
We’ve also implemented control of creating and destructing of the objects. The detailed description is given here.
When the work is completed, we have the following log:
And the most important thing is that we’ve covered part of the code with tests. Currently, there are 174 of them.
At the same time such drawings appear on saving tests in PNG :
Then we check if DUnit works using FirstTest.
Then we check if DUnit works using FirstTest.
Next, we add first tests but at once classify them as integration or unit ones.
Let’s start with the integration tests. Using the first test we’ll find out whether all our shapes are registered:
We write two more tests to check the number of shapes we need:
And now we pass on to the unit tests.
To begin with, we write the base class of the unit test.
And now I’ll briefly tell about how it all works.
Although our class is an abstract one, the whole logic is hidden here. It is inherited from TTestCase in DUnit, therefore, if you like, any descendant can be registered for testing by realizing through inheritance the unique configurations, that are not included in the context.
The sense of testing (as we understand it; and it is not TDD at all) has been described in detail in our blog by the example of testing of the elementary calculator.
In brief – using the testing with etalons means saving values and the result of the test in file, which is compared to the etalon afterwards. If files do not coincide, the test failed. This raises a question: where will we get the etalon file? We have two ways: either we create it with our own hands or (as I did), if etalon is not available, we create it automatically on the basis of the file of testing results, since we suppose (we check manually as usual in the old way by eye) that our tests are obviously correct.
The attentive reader might have noticed that lambdas and anonymous methods are fully used in the class. For us it is one of the ways to support the DRY-principle. Where it is not enough, we use inheritance. I would not say which of them is the main one (more likely, it is important to combine and recognize the most appropriate method), but I can say for sure – we follow the principle by 95%. The remaining 5% are rather laziness or common sense.
I’ll stop teasing you with theory and show you descendant classes:
As we can see, not many things have changed. In fact, we’ve just said how to change the name of the result. It has been done because we’ll use base class for all tests. Anyway, only the following ones will check serialization, the other class will “result” in *.png.
The test of the shapes.
The only important line about the test of saving in *.png is here:
The whole text of the unit:
The class for test of saving in *.png looks like this:
Again, an attentive reader who works or worked with DUnit will notice there is no registration of testing classes. This means, if we add them to the project now, nothing will take place.
We’ll introduce a new class, a “set of tests”, or, as DUnit-team has called it, TestSuite.
Here it is, our “special magic”.
The new class is inherited from TestSuite and each class is “made” unique.
The explanation of only one method will be of special value. Let’s analyze it line by line.
Thanks to everybody who have read this far and, as always, criticism and commentaries are welcome.
Repository
Table of contents
Hello, dear readership.
In this post I want to tell about the changes in our project and also about the technologies and methods we’ve used to achieve our goals.
Now our project looks as follows:
Json of the picture drawn below and saved in PNG using the program:
{ "type": "msDiagramms.TmsDiagramms",
"id": 1,
"fields": {
"f_Items": [{
"type": "msDiagramm.TmsDiagramm",
"id": 2,
"fields": {
"fName": "¹1",
"f_Items": [{
"type": "msRoundedRectangle.TmsRoundedRectangle",
"id": 3,
"fields": {
"FStartPoint": [[110,
186],
110,
186],
"f_Items": []
}
},
{
"type": "msRoundedRectangle.TmsRoundedRectangle",
"id": 4,
"fields": {
"FStartPoint": [[357,
244],
357,
244],
"f_Items": []
}
},
{
"type": "msTriangle.TmsTriangle",
"id": 5,
"fields": {
"FStartPoint": [[244,
58],
244,
58],
"f_Items": []
}
},
{
"type": "msLineWithArrow.TmsLineWithArrow",
"id": 6,
"fields": {
"FFinishPoint": [[236,
110],
236,
110],
"FStartPoint": [[156,
175],
156,
175],
"f_Items": []
}
},
{
"type": "msLineWithArrow.TmsLineWithArrow",
"id": 7,
"fields": {
"FFinishPoint": [[262,
109],
262,
109],
"FStartPoint": [[327,
199],
327,
199],
"f_Items": []
}
},
{
"type": "msUseCaseLikeEllipse.TmsUseCaseLikeEllipse",
"id": 8,
"fields": {
"FStartPoint": [[52,
334],
52,
334],
"f_Items": []
}
},
{
"type": "msUseCaseLikeEllipse.TmsUseCaseLikeEllipse",
"id": 9,
"fields": {
"FStartPoint": [[171,
336],
171,
336],
"f_Items": []
}
},
{
"type": "msLineWithArrow.TmsLineWithArrow",
"id": 10,
"fields": {
"FFinishPoint": [[98,
232],
98,
232],
"FStartPoint": [[62,
300],
62,
300],
"f_Items": []
}
},
{
"type": "msLineWithArrow.TmsLineWithArrow",
"id": 11,
"fields": {
"FFinishPoint": [[133,
233],
133,
233],
"FStartPoint": [[167,
299],
167,
299],
"f_Items": []
}
},
{
"type": "msRectangle.TmsRectangle",
"id": 12,
"fields": {
"FStartPoint": [[302,
395],
302,
395],
"f_Items": []
}
},
{
"type": "msRectangle.TmsRectangle",
"id": 13,
"fields": {
"FStartPoint": [[458,
389],
458,
389],
"f_Items": []
}
},
{
"type": "msLineWithArrow.TmsLineWithArrow",
"id": 14,
"fields": {
"FFinishPoint": [[361,
292],
361,
292],
"FStartPoint": [[308,
351],
308,
351],
"f_Items": []
}
},
{
"type": "msLineWithArrow.TmsLineWithArrow",
"id": 15,
"fields": {
"FFinishPoint": [[389,
292],
389,
292],
"FStartPoint": [[455,
344],
455,
344],
"f_Items": []
}
},
{
"type": "msCircle.TmsCircle",
"id": 16,
"fields": {
"FStartPoint": [[58,
51],
58,
51],
"f_Items": []
}
},
{
"type": "msLineWithArrow.TmsLineWithArrow",
"id": 17,
"fields": {
"FFinishPoint": [[88,
94],
88,
94],
"FStartPoint": [[108,
141],
108,
141],
"f_Items": []
}
}]
}
}]
}
}
Each shape is now capable of “being the diagram”. It means we can choose a shape and build a new diagram “inside”. It is clearly demonstrated below.
The object TmsPicker is responsible for the capability of “falling into”. The object TmsUpToParrent is responsible for the returning to the parent diagram.
We’ve also got ToolBar in which all shapes intended for drawing are drawn dynamically. It also realizes the capability of creating special shapes, for example, for the object of transfer (under the red square):
We’ve also implemented control of creating and destructing of the objects. The detailed description is given here.
When the work is completed, we have the following log:
Lost objects: 0 TmsPaletteShape Lost: 0 Max objects of class used: 5 TmsPaletteShapeCreator Lost: 0 Max objects of class used: 1 TmsUpArrow Lost: 0 Max objects of class used: 1 TmsDashDotLine Lost: 0 Max objects of class used: 164 TmsLine Lost: 0 Max objects of class used: 278 TmsRectangle Lost: 0 Max objects of class used: 144 TmsCircle Lost: 0 Max objects of class used: 908 TmsLineWithArrow Lost: 0 Max objects of class used: 309 TmsDiagrammsController Lost: 0 Max objects of class used: 1 TmsStringList Lost: 0 Max objects of class used: 3 TmsCompletedShapeCreator Lost: 0 Max objects of class used: 2 TmsRoundedRectangle Lost: 0 Max objects of class used: 434 TmsTriangleDirectionRight Lost: 0 Max objects of class used: 5 TmsGreenCircle Lost: 0 Max objects of class used: 850 TmsSmallTriangle Lost: 0 Max objects of class used: 761 TmsShapeCreator Lost: 0 Max objects of class used: 1 TmsDashLine Lost: 0 Max objects of class used: 868 TmsGreenRectangle Lost: 0 Max objects of class used: 759 TmsDiagramm Lost: 0 Max objects of class used: 910 TmsDownArrow Lost: 0 Max objects of class used: 1 TmsDotLine Lost: 0 Max objects of class used: 274 TmsDiagramms Lost: 0 Max objects of class used: 3 TmsDiagrammsHolder Lost: 0 Max objects of class used: 18 TmsPointCircle Lost: 0 Max objects of class used: 717 TmsUseCaseLikeEllipse Lost: 0 Max objects of class used: 397 TmsBlackTriangle Lost: 0 Max objects of class used: 43 TmsRedRectangle Lost: 0 Max objects of class used: 139 TmsMoverIcon Lost: 0 Max objects of class used: 220 TmsTriangle Lost: 0 Max objects of class used: 437
And the most important thing is that we’ve covered part of the code with tests. Currently, there are 174 of them.
At the same time such drawings appear on saving tests in PNG :
The size of the “etalon” of checking the drawing of a red circle: 1048x2049 pixels. The file size is 1.7 MB.
But the details will be given further.
Let us start in reverse order.
The tests.
First of all, we add DUnit to the project. To do this, we add one line to the project, and then it looks like this:
program MindStream; uses FMX.Forms, … ; begin Application.Initialize; Application.CreateForm(TfmMain, fmMain); // We add our GUI_Runner that will, in its turn, find all unregistered tests u_fmGUITestRunner.RunRegisteredTestsModeless; Application.Run; end.
Then we check if DUnit works using FirstTest.
unit FirstTest;
interface
uses
TestFrameWork;
type
TFirstTest = class(TTestCase)
published
procedure DoIt;
end; // TFirstTest
implementation
uses
SysUtils;
procedure TFirstTest.DoIt;
begin
Check(true);
end;
initialization
TestFrameWork.RegisterTest(TFirstTest.Suite);
end.
Then we check if DUnit works using FirstTest.
unit FirstTest;
interface
uses
TestFrameWork;
type
TFirstTest = class(TTestCase)
published
procedure DoIt;
end; // TFirstTest
implementation
uses
SysUtils;
procedure TFirstTest.DoIt;
begin
Check(true);
end;
initialization
TestFrameWork.RegisterTest(TFirstTest.Suite);
end.
Next, we add first tests but at once classify them as integration or unit ones.
Let’s start with the integration tests. Using the first test we’ll find out whether all our shapes are registered:
unit RegisteredShapesTest;
interface
uses
TestFrameWork;
type
TRegisteredShapesTest = class(TTestCase)
published
procedure ShapesRegistredCount;
procedure TestFirstShape;
procedure TestIndexOfTmsLine;
end; // TRegisteredShapesTest
implementation
uses
SysUtils,
msRegisteredShapes,
msShape,
msLine,
FMX.Objects,
FMX.Graphics;
procedure TRegisteredShapesTest.ShapesRegistredCount;
var
l_Result: integer;
begin
l_Result := 0;
TmsRegisteredShapes.IterateShapes(
procedure(aShapeClass: RmsShape)
begin
Inc(l_Result);
end);
CheckTrue(l_Result = 23, ' Expected 23 - Get ' + IntToStr(l_Result));
end;
procedure TRegisteredShapesTest.TestFirstShape;
begin
CheckTrue(TmsRegisteredShapes.Instance.First = TmsLine);
end;
procedure TRegisteredShapesTest.TestIndexOfTmsLine;
begin
CheckTrue(TmsRegisteredShapes.Instance.IndexOf(TmsLine) = 0);
end;
initialization
TestFrameWork.RegisterTest(TRegisteredShapesTest.Suite);
end.
We write two more tests to check the number of shapes we need:
...
type
TUtilityShapesTest = class(TTestCase)
published
procedure ShapesRegistredCount;
procedure TestFirstShape;
procedure TestIndexOfTmsLine;
end; // TUtilityShapesTest
...
procedure TUtilityShapesTest.ShapesRegistredCount;
var
l_Result: integer;
begin
l_Result := 0;
TmsUtilityShapes.IterateShapes(
procedure(aShapeClass: RmsShape)
begin
Assert(aShapeClass.IsForToolbar);
Inc(l_Result);
end);
CheckTrue(l_Result = 5, ' Expected 5 - Get ' + IntToStr(l_Result));
end;
…
TForToolbarShapesTest = class(TTestCase)
published
procedure ShapesRegistredCount;
procedure TestFirstShape;
procedure TestIndexOfTmsLine;
end; // TForToolbarShapesTest
procedure TForToolbarShapesTest.ShapesRegistredCount;
var
l_Result: integer;
begin
l_Result := 0;
TmsShapesForToolbar.IterateShapes(
procedure(aShapeClass: RmsShape)
begin
Assert(aShapeClass.IsForToolbar);
Inc(l_Result);
end);
CheckTrue(l_Result = 18, ' Expected 18 - Get ' + IntToStr(l_Result));
end;
And now we pass on to the unit tests.
To begin with, we write the base class of the unit test.
type
TmsShapeClassCheck = TmsShapeClassLambda;
TmsDiagrammCheck = reference to procedure(const aDiagramm: ImsDiagramm);
TmsDiagrammSaveTo = reference to procedure(const aFileName: String; const aDiagramm: ImsDiagramm);
// The context of testing stores all unique information for each test
TmsShapeTestContext = record
rMethodName: string;
rSeed: Integer;
rDiagrammName: String;
rShapesCount: Integer;
rShapeClass: RmsShape;
constructor Create(aMethodName: string;
aSeed: Integer; aDiagrammName: string; aShapesCount: Integer; aShapeClass: RmsShape);
end; // TmsShapeTestContext
TmsShapeTestPrim = class abstract(TTestCase)
protected
// The context of testing stores all unique information for each test
f_Context: TmsShapeTestContext;
f_TestSerializeMethodName: String;
f_Coords: array of TPoint;
protected
class function ComputerName: AnsiString;
function TestResultsFileName: String; virtual;
function MakeFileName(const aTestName: string; const aTestFolder: string): String; virtual;
procedure CreateDiagrammAndCheck(aCheck: TmsDiagrammCheck; const aName: String);
// The procedure of checking the results of the test with the etalone
procedure CheckFileWithEtalon(const aFileName: String);
procedure SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm); virtual;
procedure SaveDiagrammAndCheck(const aDiagramm: ImsDiagramm; aSaveTo: TmsDiagrammSaveTo);
procedure OutToFileAndCheck(aLambda: TmsLogLambda);
procedure SetUp; override;
function ShapesCount: Integer;
procedure CreateDiagrammWithShapeAndSaveAndCheck;
function TestSerializeMethodName: String;
procedure DeserializeDiargammAndCheck(aCheck: TmsDiagrammCheck);
procedure TestDeSerializeForShapeClass;
procedure TestDeSerializeViaShapeCheckForShapeClass;
public
class procedure CheckShapes(aCheck: TmsShapeClassCheck);
constructor Create(const aContext: TmsShapeTestContext);
end; // TmsShapeTestPrim
function TmsShapeTestPrim.MakeFileName(const aTestName: string; const aTestFolder: string): String;
var
l_Folder: String;
begin
l_Folder := ExtractFilePath(ParamStr(0)) + 'TestResults\' + aTestFolder;
ForceDirectories(l_Folder);
Result := l_Folder + ClassName + '_' + aTestName + '_' + f_Context.rShapeClass.ClassName;
end;
procedure TmsShapeTestPrim.CheckFileWithEtalon(const aFileName: String);
var
l_FileNameEtalon: String;
begin
l_FileNameEtalon := aFileName + '.etalon' + ExtractFileExt(aFileName);
if FileExists(l_FileNameEtalon) then
begin
CheckTrue(msCompareFiles(l_FileNameEtalon, aFileName));
end // FileExists(l_FileNameEtalon)
else
begin
CopyFile(PWideChar(aFileName), PWideChar(l_FileNameEtalon), True);
end; // FileExists(l_FileNameEtalon)
end;
const
c_JSON = 'JSON\';
function TmsShapeTestPrim.TestResultsFileName: String;
begin
Result := MakeFileName(Name, c_JSON);
end;
class function TmsShapeTestPrim.ComputerName: AnsiString;
var
l_CompSize: Integer;
begin
l_CompSize := MAX_COMPUTERNAME_LENGTH + 1;
SetLength(Result, l_CompSize);
Win32Check(GetComputerNameA(PAnsiChar(Result), LongWord(l_CompSize)));
SetLength(Result, l_CompSize);
end;
procedure TmsShapeTestPrim.SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm);
begin
aDiagramm.SaveTo(aFileName);
end;
procedure TmsShapeTestPrim.SaveDiagrammAndCheck(const aDiagramm: ImsDiagramm; aSaveTo: TmsDiagrammSaveTo);
var
l_FileNameTest: String;
begin
l_FileNameTest := TestResultsFileName;
aSaveTo(l_FileNameTest, aDiagramm);
CheckFileWithEtalon(l_FileNameTest);
end;
function TmsShapeTestPrim.ShapesCount: Integer;
begin
Result := f_Context.rShapesCount;
end;
constructor TmsShapeTestContext.Create(aMethodName: string; aSeed: Integer; aDiagrammName: string; aShapesCount: Integer;
aShapeClass: RmsShape);
begin
rMethodName := aMethodName;
rSeed := aSeed;
rDiagrammName := aDiagrammName;
rShapesCount := aShapesCount;
rShapeClass := aShapeClass;
end;
procedure TmsShapeTestPrim.SetUp;
var
l_Index: Integer;
l_X: Integer;
l_Y: Integer;
begin
inherited;
RandSeed := f_Context.rSeed;
SetLength(f_Coords, ShapesCount);
for l_Index := 0 to Pred(ShapesCount) do
begin
l_X := Random(c_MaxCanvasWidth);
l_Y := Random(c_MaxCanvasHeight);
f_Coords[l_Index] := TPoint.Create(l_X, l_Y);
end; // for l_Index
end;
procedure TmsShapeTestPrim.CreateDiagrammAndCheck(aCheck: TmsDiagrammCheck; const aName: String);
var
l_Diagramm: ImsDiagramm;
begin
l_Diagramm := TmsDiagramm.Create(aName);
try
aCheck(l_Diagramm);
finally
l_Diagramm := nil;
end; // try..finally
end;
procedure TmsShapeTestPrim.CreateDiagrammWithShapeAndSaveAndCheck;
begin
CreateDiagrammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
var
l_P: TPoint;
begin
for l_P in f_Coords do
aDiagramm.AddShape(TmsCompletedShapeCreator.Create(f_Context.rShapeClass)
.CreateShape(TmsMakeShapeContext.Create(TPointF.Create(l_P.X, l_P.Y), nil, nil))).AddNewDiagramm;
SaveDiagrammAndCheck(aDiagramm, SaveDiagramm);
end, f_Context.rDiagrammName);
end;
function TmsCustomShapeTest.MakeFileName(const aTestName: string; const aFileExtension: string): String;
begin
Result := inherited + '.json';
end;
function TmsShapeTestPrim.TestSerializeMethodName: String;
begin
Result := f_TestSerializeMethodName + 'TestSerialize';
end;
procedure TmsShapeTestPrim.DeserializeDiargammAndCheck(aCheck: TmsDiagrammCheck);
begin
CreateDiagrammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
begin
aDiagramm.LoadFrom(MakeFileName(TestSerializeMethodName, c_JSON));
// We use the results of the PREVIOUS tests, which is incorrect in terms of TDD
// BUT! It is terribly effective.
aCheck(aDiagramm);
end, '');
end;
procedure TmsShapeTestPrim.TestDeSerializeForShapeClass;
begin
DeserializeDiargammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
begin
SaveDiagrammAndCheck(aDiagramm, SaveDiagramm);
end);
end;
constructor TmsShapeTestPrim.Create(const aContext: TmsShapeTestContext);
begin
inherited Create(aContext.rMethodName);
f_Context := aContext;
FTestName := f_Context.rShapeClass.ClassName + '.' + aContext.rMethodName;
f_TestSerializeMethodName := f_Context.rShapeClass.ClassName + '.';
end;
procedure TmsShapeTestPrim.TestDeSerializeViaShapeCheckForShapeClass;
begin
DeserializeDiargammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
var
l_Shape: ImsShape;
l_Index: Integer;
begin
Check(aDiagramm.Name = f_Context.rDiagrammName);
Check(Length(f_Coords) = aDiagramm.ItemsCount);
l_Index := 0;
for l_Shape in aDiagramm do
begin
Check(l_Shape.ClassType = f_Context.rShapeClass);
Check(l_Shape.StartPoint.X = f_Coords[l_Index].X);
Check(l_Shape.StartPoint.Y = f_Coords[l_Index].Y);
Inc(l_Index);
end; // for l_Shape
end);
end;
procedure TmsShapeTestPrim.OutToFileAndCheck(aLambda: TmsLogLambda);
var
l_FileNameTest: String;
begin
l_FileNameTest := TestResultsFileName;
TmsLog.Log(l_FileNameTest,
procedure(aLog: TmsLog)
begin
aLambda(aLog);
end);
CheckFileWithEtalon(l_FileNameTest);
end;
class procedure TmsShapeTestPrim.CheckShapes(aCheck: TmsShapeClassCheck);
begin
TmsRegisteredShapes.IterateShapes(
procedure(aShapeClass: RmsShape)
begin
if not aShapeClass.IsTool then
aCheck(aShapeClass);
end);
end;
And now I’ll briefly tell about how it all works.
Although our class is an abstract one, the whole logic is hidden here. It is inherited from TTestCase in DUnit, therefore, if you like, any descendant can be registered for testing by realizing through inheritance the unique configurations, that are not included in the context.
The sense of testing (as we understand it; and it is not TDD at all) has been described in detail in our blog by the example of testing of the elementary calculator.
In brief – using the testing with etalons means saving values and the result of the test in file, which is compared to the etalon afterwards. If files do not coincide, the test failed. This raises a question: where will we get the etalon file? We have two ways: either we create it with our own hands or (as I did), if etalon is not available, we create it automatically on the basis of the file of testing results, since we suppose (we check manually as usual in the old way by eye) that our tests are obviously correct.
The attentive reader might have noticed that lambdas and anonymous methods are fully used in the class. For us it is one of the ways to support the DRY-principle. Where it is not enough, we use inheritance. I would not say which of them is the main one (more likely, it is important to combine and recognize the most appropriate method), but I can say for sure – we follow the principle by 95%. The remaining 5% are rather laziness or common sense.
I’ll stop teasing you with theory and show you descendant classes:
RmsShapeTest = class of TmsShapeTestPrim;
TmsCustomShapeTest = class(TmsShapeTestPrim)
protected
function MakeFileName(const aTestName: string; const aFileExtension: string): String; override;
published
procedure TestSerialize;
end; // TmsCustomShapeTest
function TmsCustomShapeTest.MakeFileName(const aTestName: string; const aFileExtension: string): String;
begin
Result := inherited + '.json';
end;
procedure TmsCustomShapeTest.TestSerialize;
begin
CreateDiagrammWithShapeAndSaveAndCheck;
end;
As we can see, not many things have changed. In fact, we’ve just said how to change the name of the result. It has been done because we’ll use base class for all tests. Anyway, only the following ones will check serialization, the other class will “result” in *.png.
TmsDiagrammTest = class(TmsCustomShapeTest)
protected
procedure SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm); override;
published
procedure TestDeSerialize;
end; // TmsDiagrammTest
procedure TmsDiagrammTest.SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm);
var
l_Diagramms: ImsDiagramms;
begin
l_Diagramms := TmsDiagramms.Create;
try
l_Diagramms.AddDiagramm(aDiagramm);
l_Diagramms.SaveTo(aFileName);
finally
l_Diagramms := nil;
end; // try..finally
end;
procedure TmsDiagrammTest.TestDeSerialize;
var
l_Diagramms: ImsDiagramms;
l_FileName: String;
begin
l_Diagramms := TmsDiagramms.Create;
try
l_Diagramms.LoadFrom(MakeFileName(TestSerializeMethodName, c_JSON));
// We use the results of the PREVIOUS tests, which is incorrect in terms of TDD
// BUT! It is terribly effective.
l_FileName := TestResultsFileName;
l_Diagramms.SaveTo(l_FileName);
CheckFileWithEtalon(l_FileName);
finally
l_Diagramms := nil;
end; // try..finally
end;
The test of the shapes.
TmsShapeTest = class(TmsCustomShapeTest)
published
procedure TestDeSerialize;
procedure TestDeSerializeViaShapeCheck;
procedure TestShapeName;
procedure TestDiagrammName;
end; // TmsShapeTest
procedure TmsShapeTest.TestDeSerializeViaShapeCheck;
begin
TestDeSerializeViaShapeCheckForShapeClass;
end;
procedure TmsShapeTest.TestDeSerialize;
begin
TestDeSerializeForShapeClass;
end;
procedure TmsShapeTest.TestShapeName;
begin
OutToFileAndCheck(
procedure(aLog: TmsLog)
begin
aLog.ToLog(f_Context.rShapeClass.ClassName);
end);
end;
procedure TmsShapeTest.TestDiagrammName;
begin
OutToFileAndCheck(
procedure(aLog: TmsLog)
begin
aLog.ToLog(f_Context.rDiagrammName);
end);
end;
The only important line about the test of saving in *.png is here:
function TTestSaveToPNG.TestResultsFileName: String; const c_PNG = 'PNG\'; begin // Since my college and I work on different monitors and so with different resolutions, we’re cheating a bit. Again, taking the common sense into account. Result := MakeFileName(Name, c_PNG + ComputerName + '\'); end;
The whole text of the unit:
unit msShapeTest;
interface
uses
TestFramework,
msDiagramm,
msShape,
msRegisteredShapes,
System.Types,
System.Classes,
msCoreObjects,
msInterfaces;
type
TmsShapeClassCheck = TmsShapeClassLambda;
TmsDiagrammCheck = reference to procedure(const aDiagramm: ImsDiagramm);
TmsDiagrammSaveTo = reference to procedure(const aFileName: String; const aDiagramm: ImsDiagramm);
TmsShapeTestContext = record
rMethodName: string;
rSeed: Integer;
rDiagrammName: String;
rShapesCount: Integer;
rShapeClass: RmsShape;
constructor Create(aMethodName: string;
aSeed: Integer; aDiagrammName: string; aShapesCount: Integer; aShapeClass: RmsShape);
end; // TmsShapeTestContext
TmsShapeTestPrim = class abstract(TTestCase)
protected
f_Context: TmsShapeTestContext;
f_TestSerializeMethodName: String;
f_Coords: array of TPoint;
protected
class function ComputerName: AnsiString;
function TestResultsFileName: String; virtual;
function MakeFileName(const aTestName: string; const aTestFolder: string): String; virtual;
procedure CreateDiagrammAndCheck(aCheck: TmsDiagrammCheck; const aName: String);
procedure CheckFileWithEtalon(const aFileName: String);
procedure SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm); virtual;
procedure SaveDiagrammAndCheck(const aDiagramm: ImsDiagramm; aSaveTo: TmsDiagrammSaveTo);
procedure OutToFileAndCheck(aLambda: TmsLogLambda);
procedure SetUp; override;
function ShapesCount: Integer;
procedure CreateDiagrammWithShapeAndSaveAndCheck;
function TestSerializeMethodName: String;
procedure DeserializeDiargammAndCheck(aCheck: TmsDiagrammCheck);
procedure TestDeSerializeForShapeClass;
procedure TestDeSerializeViaShapeCheckForShapeClass;
public
class procedure CheckShapes(aCheck: TmsShapeClassCheck);
constructor Create(const aContext: TmsShapeTestContext);
end; // TmsShapeTestPrim
RmsShapeTest = class of TmsShapeTestPrim;
TmsCustomShapeTest = class(TmsShapeTestPrim)
protected
function MakeFileName(const aTestName: string; const aFileExtension: string): String; override;
published
procedure TestSerialize;
end; // TmsCustomShapeTest
TmsDiagrammTest = class(TmsCustomShapeTest)
protected
procedure SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm); override;
published
procedure TestDeSerialize;
end; // TmsDiagrammTest
TmsShapeTest = class(TmsCustomShapeTest)
published
procedure TestDeSerialize;
procedure TestDeSerializeViaShapeCheck;
procedure TestShapeName;
procedure TestDiagrammName;
end; // TmsShapeTest
implementation
uses
System.SysUtils,
Winapi.Windows,
System.Rtti,
System.TypInfo,
FMX.Objects,
msSerializeInterfaces,
msDiagrammMarshal,
msDiagrammsMarshal,
msStringList,
msDiagramms,
Math,
msStreamUtils,
msTestConstants,
msShapeCreator,
msCompletedShapeCreator;
function TmsShapeTestPrim.MakeFileName(const aTestName: string; const aTestFolder: string): String;
var
l_Folder: String;
begin
l_Folder := ExtractFilePath(ParamStr(0)) + 'TestResults\' + aTestFolder;
ForceDirectories(l_Folder);
Result := l_Folder + ClassName + '_' + aTestName + '_' + f_Context.rShapeClass.ClassName;
end;
procedure TmsShapeTestPrim.CheckFileWithEtalon(const aFileName: String);
var
l_FileNameEtalon: String;
begin
l_FileNameEtalon := aFileName + '.etalon' + ExtractFileExt(aFileName);
if FileExists(l_FileNameEtalon) then
begin
CheckTrue(msCompareFiles(l_FileNameEtalon, aFileName));
end // FileExists(l_FileNameEtalon)
else
begin
CopyFile(PWideChar(aFileName), PWideChar(l_FileNameEtalon), True);
end; // FileExists(l_FileNameEtalon)
end;
const
c_JSON = 'JSON\';
function TmsShapeTestPrim.TestResultsFileName: String;
begin
Result := MakeFileName(Name, c_JSON);
end;
class function TmsShapeTestPrim.ComputerName: AnsiString;
var
l_CompSize: Integer;
begin
l_CompSize := MAX_COMPUTERNAME_LENGTH + 1;
SetLength(Result, l_CompSize);
Win32Check(GetComputerNameA(PAnsiChar(Result), LongWord(l_CompSize)));
SetLength(Result, l_CompSize);
end;
procedure TmsShapeTestPrim.SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm);
begin
aDiagramm.SaveTo(aFileName);
end;
procedure TmsShapeTestPrim.SaveDiagrammAndCheck(const aDiagramm: ImsDiagramm; aSaveTo: TmsDiagrammSaveTo);
var
l_FileNameTest: String;
begin
l_FileNameTest := TestResultsFileName;
aSaveTo(l_FileNameTest, aDiagramm);
CheckFileWithEtalon(l_FileNameTest);
end;
function TmsShapeTestPrim.ShapesCount: Integer;
begin
Result := f_Context.rShapesCount;
end;
constructor TmsShapeTestContext.Create(aMethodName: string; aSeed: Integer; aDiagrammName: string; aShapesCount: Integer;
aShapeClass: RmsShape);
begin
rMethodName := aMethodName;
rSeed := aSeed;
rDiagrammName := aDiagrammName;
rShapesCount := aShapesCount;
rShapeClass := aShapeClass;
end;
procedure TmsShapeTestPrim.SetUp;
var
l_Index: Integer;
l_X: Integer;
l_Y: Integer;
begin
inherited;
RandSeed := f_Context.rSeed;
SetLength(f_Coords, ShapesCount);
for l_Index := 0 to Pred(ShapesCount) do
begin
l_X := Random(c_MaxCanvasWidth);
l_Y := Random(c_MaxCanvasHeight);
f_Coords[l_Index] := TPoint.Create(l_X, l_Y);
end; // for l_Index
end;
procedure TmsShapeTestPrim.CreateDiagrammAndCheck(aCheck: TmsDiagrammCheck; const aName: String);
var
l_Diagramm: ImsDiagramm;
begin
l_Diagramm := TmsDiagramm.Create(aName);
try
aCheck(l_Diagramm);
finally
l_Diagramm := nil;
end; // try..finally
end;
procedure TmsShapeTestPrim.CreateDiagrammWithShapeAndSaveAndCheck;
begin
CreateDiagrammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
var
l_P: TPoint;
begin
for l_P in f_Coords do
aDiagramm.AddShape(TmsCompletedShapeCreator.Create(f_Context.rShapeClass)
.CreateShape(TmsMakeShapeContext.Create(TPointF.Create(l_P.X, l_P.Y), nil, nil))).AddNewDiagramm;
SaveDiagrammAndCheck(aDiagramm, SaveDiagramm);
end, f_Context.rDiagrammName);
end;
function TmsCustomShapeTest.MakeFileName(const aTestName: string; const aFileExtension: string): String;
begin
Result := inherited + '.json';
end;
procedure TmsCustomShapeTest.TestSerialize;
begin
CreateDiagrammWithShapeAndSaveAndCheck;
end;
function TmsShapeTestPrim.TestSerializeMethodName: String;
begin
Result := f_TestSerializeMethodName + 'TestSerialize';
end;
procedure TmsShapeTestPrim.DeserializeDiargammAndCheck(aCheck: TmsDiagrammCheck);
begin
CreateDiagrammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
begin
aDiagramm.LoadFrom(MakeFileName(TestSerializeMethodName, c_JSON));
// We use the results of the PREVIOUS tests, which is incorrect in terms of TDD
// BUT! It is terribly effective.
aCheck(aDiagramm);
end, '');
end;
procedure TmsShapeTestPrim.TestDeSerializeForShapeClass;
begin
DeserializeDiargammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
begin
SaveDiagrammAndCheck(aDiagramm, SaveDiagramm);
end);
end;
procedure TmsShapeTest.TestDeSerialize;
begin
TestDeSerializeForShapeClass;
end;
constructor TmsShapeTestPrim.Create(const aContext: TmsShapeTestContext);
begin
inherited Create(aContext.rMethodName);
f_Context := aContext;
FTestName := f_Context.rShapeClass.ClassName + '.' + aContext.rMethodName;
f_TestSerializeMethodName := f_Context.rShapeClass.ClassName + '.';
end;
procedure TmsShapeTestPrim.TestDeSerializeViaShapeCheckForShapeClass;
begin
DeserializeDiargammAndCheck(
procedure(const aDiagramm: ImsDiagramm)
var
l_Shape: ImsShape;
l_Index: Integer;
begin
Check(aDiagramm.Name = f_Context.rDiagrammName);
Check(Length(f_Coords) = aDiagramm.ItemsCount);
l_Index := 0;
for l_Shape in aDiagramm do
begin
Check(l_Shape.ClassType = f_Context.rShapeClass);
Check(l_Shape.StartPoint.X = f_Coords[l_Index].X);
Check(l_Shape.StartPoint.Y = f_Coords[l_Index].Y);
Inc(l_Index);
end; // for l_Shape
end);
end;
procedure TmsShapeTest.TestDeSerializeViaShapeCheck;
begin
TestDeSerializeViaShapeCheckForShapeClass;
end;
procedure TmsShapeTestPrim.OutToFileAndCheck(aLambda: TmsLogLambda);
var
l_FileNameTest: String;
begin
l_FileNameTest := TestResultsFileName;
TmsLog.Log(l_FileNameTest,
procedure(aLog: TmsLog)
begin
aLambda(aLog);
end);
CheckFileWithEtalon(l_FileNameTest);
end;
procedure TmsShapeTest.TestShapeName;
begin
OutToFileAndCheck(
procedure(aLog: TmsLog)
begin
aLog.ToLog(f_Context.rShapeClass.ClassName);
end);
end;
procedure TmsShapeTest.TestDiagrammName;
begin
OutToFileAndCheck(
procedure(aLog: TmsLog)
begin
aLog.ToLog(f_Context.rDiagrammName);
end);
end;
class procedure TmsShapeTestPrim.CheckShapes(aCheck: TmsShapeClassCheck);
begin
TmsRegisteredShapes.IterateShapes(
procedure(aShapeClass: RmsShape)
begin
if not aShapeClass.IsTool then
aCheck(aShapeClass);
end);
end;
// TmsDiagrammTest
procedure TmsDiagrammTest.SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm);
var
l_Diagramms: ImsDiagramms;
begin
l_Diagramms := TmsDiagramms.Create;
try
l_Diagramms.AddDiagramm(aDiagramm);
l_Diagramms.SaveTo(aFileName);
finally
l_Diagramms := nil;
end; // try..finally
end;
procedure TmsDiagrammTest.TestDeSerialize;
var
l_Diagramms: ImsDiagramms;
l_FileName: String;
begin
l_Diagramms := TmsDiagramms.Create;
try
l_Diagramms.LoadFrom(MakeFileName(TestSerializeMethodName, c_JSON));
// We use the results of the PREVIOUS tests, which is incorrect in terms of TDD
// BUT! It is terribly effective.
l_FileName := TestResultsFileName;
l_Diagramms.SaveTo(l_FileName);
CheckFileWithEtalon(l_FileName);
finally
l_Diagramms := nil;
end; // try..finally
end;
end.
The class for test of saving in *.png looks like this:
unit TestSaveToPNG;
interface
uses
TestFrameWork,
msShapeTest,
msInterfaces;
type
TTestSaveToPNG = class(TmsShapeTestPrim)
protected
function MakeFileName(const aTestName: string; const aTestFolder: string): String; override;
function TestResultsFileName: String; override;
procedure SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm); override;
published
procedure CreateDiagrammWithShapeAndSaveToPNG_AndCheck;
end; // TTestSaveToPNG
implementation
uses
SysUtils,
System.Types,
msRegisteredShapes,
FMX.Graphics;
{ TTestSaveToPNG }
procedure TTestSaveToPNG.SaveDiagramm(const aFileName: String; const aDiagramm: ImsDiagramm);
begin
aDiagramm.SaveToPng(aFileName);
end;
procedure TTestSaveToPNG.CreateDiagrammWithShapeAndSaveToPNG_AndCheck;
begin
CreateDiagrammWithShapeAndSaveAndCheck;
end;
function TTestSaveToPNG.MakeFileName(const aTestName: string; const aTestFolder: string): String;
begin
Result := inherited + '.png';
end;
function TTestSaveToPNG.TestResultsFileName: String;
const
c_PNG = 'PNG\';
begin
Result := MakeFileName(Name, c_PNG + ComputerName + '\');
end;
initialization
end.
Again, an attentive reader who works or worked with DUnit will notice there is no registration of testing classes. This means, if we add them to the project now, nothing will take place.
We’ll introduce a new class, a “set of tests”, or, as DUnit-team has called it, TestSuite.
Here it is, our “special magic”.
The new class is inherited from TestSuite and each class is “made” unique.
unit msShapeTestSuite;
interface
uses
TestFramework,
msShape,
msShapeTest;
type
TmsParametrizedShapeTestSuite = class(TTestSuite)
private
constructor CreatePrim;
protected
class function TestClass: RmsShapeTest; virtual; abstract;
public
procedure AddTests(TestClass: TTestCaseClass); override;
class function Create: ITest;
end; // TmsParametrizedShapeTestSuite
TmsShapesTest = class(TmsParametrizedShapeTestSuite)
protected
class function TestClass: RmsShapeTest; override;
end; // TmsShapesTest
TmsDiagrammsTest = class(TmsParametrizedShapeTestSuite)
protected
class function TestClass: RmsShapeTest; override;
end; // TmsDiagrammsTest
TmsDiagrammsToPNGTest = class(TmsParametrizedShapeTestSuite)
protected
class function TestClass: RmsShapeTest; override;
end; // TmsDiagrammsTest
implementation
uses
System.TypInfo,
System.Rtti,
SysUtils,
TestSaveToPNG;
// TmsShapesTest
class function TmsShapesTest.TestClass: RmsShapeTest;
begin
Result := TmsShapeTest;
end;
// TmsDiagrammsTest
class function TmsDiagrammsTest.TestClass: RmsShapeTest;
begin
Result := TmsDiagrammTest;
end;
// TmsParametrizedShapeTestSuite
constructor TmsParametrizedShapeTestSuite.CreatePrim;
begin
inherited Create(TestClass);
end;
class function TmsParametrizedShapeTestSuite.Create: ITest;
begin
Result := CreatePrim;
end;
procedure TmsParametrizedShapeTestSuite.AddTests(TestClass: TTestCaseClass);
begin
Assert(TestClass.InheritsFrom(TmsShapeTestPrim));
RandSeed := 10;
TmsShapeTestPrim.CheckShapes(
procedure(aShapeClass: RmsShape)
var
l_Method: TRttiMethod;
l_DiagrammName: String;
l_Seed: Integer;
l_ShapesCount: Integer;
begin
l_Seed := Random(High(l_Seed));
l_DiagrammName := 'Diagram ' + IntToStr(Random(10));
l_ShapesCount := Random(1000) + 1;
for l_Method in TRttiContext.Create.GetType(TestClass).GetMethods do
if (l_Method.Visibility = mvPublished) then
AddTest(RmsShapeTest(TestClass).Create(TmsShapeTestContext.Create(l_Method.Name, l_Seed, l_DiagrammName, l_ShapesCount,
aShapeClass)));
end);
end;
{ TmsDiagrammsToPNGTest }
class function TmsDiagrammsToPNGTest.TestClass: RmsShapeTest;
begin
Result := TTestSaveToPNG;
end;
initialization
// That is where the registration is!!!
RegisterTest(TmsShapesTest.Create);
RegisterTest(TmsDiagrammsTest.Create);
RegisterTest(TmsDiagrammsToPNGTest.Create);
end.
The explanation of only one method will be of special value. Let’s analyze it line by line.
procedure TmsParametrizedShapeTestSuite.AddTests(TestClass: TTestCaseClass);
begin
// The contract
Assert(TestClass.InheritsFrom(TmsShapeTestPrim));
// We set Random
RandSeed := 10;
// We create tests taking into account the context of testing
TmsShapeTestPrim.CheckShapes(
procedure(aShapeClass: RmsShape)
var
l_Method: TRttiMethod;
l_DiagrammName: String;
l_Seed: Integer;
l_ShapesCount: Integer;
begin
// We create the “unique” context! It is important!
// We set Random
l_Seed := Random(High(l_Seed));
// We generate the unique name for the diagram
l_DiagrammName := 'Diagram ' + IntToStr(Random(10));
// We set discrepancy in the number of shapes
l_ShapesCount := Random(1000) + 1;
// We apply the new RTTI to solve our problems (it is really that simple) and then call the required test with required parameters (context)
for l_Method in TRttiContext.Create.GetType(TestClass).GetMethods do
if (l_Method.Visibility = mvPublished) then
AddTest(RmsShapeTest(TestClass).Create(TmsShapeTestContext.Create(l_Method.Name,
l_Seed,
l_DiagrammName,
l_ShapesCount,
aShapeClass)));
end);
end;
Thanks to everybody who have read this far and, as always, criticism and commentaries are welcome.
Repository








Комментариев нет:
Отправить комментарий