15 января 2015 в 22:37

MindStream. Как мы пишем ПО под FireMonkey. Часть 5. Тестирование

Часть 1.
Часть 2.
Часть 3. DUnit + FireMonkey
Часть 3.1. По мотивам GUIRunner
Часть 4. Serialization

Здравствуйте, дорогие хабровчане.

В этом посте я хочу рассказать об изменениях, которые произошли с нашим проектом, а также о технологиях и приемах, которые мы использовали для достижения наших целей.

Сейчас наш проект выглядит так:



Диаграмму можно сохранить в Json, а также восстановить из Json, о чём я писал в предыдущей статье.
Json картинки, нарисованной ниже и сохраненной в PNG благодаря программе:
{
	"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": []
					}
				}]
			}
		}]
	}
}




Каждая фигура стала обладать возможностью “быть диаграммой”. То есть, мы можем выбрать фигуру и построить “внутри” новую диаграмму. Более наглядно продемонстрировано ниже.

Объект TmsPicker отвечает за возможность “проваливания внутрь”. Объект TmsUpToParrent отвечает за возвращение к родительской диаграмме.

image

Также у нас появился ToolBar, в котором динамически рисуются все фигуры, предназначенные для рисования, и реализована возможность создавать специальные фигуры, например, для объекта перемещения (под красным квадратом):



Также нами был реализован контроль за созданием\освобождением объектов. Детальное описание
тут.
После окончания работы приложения получаем такой лог:
MindStream.exe.objects.log
Неосвобождено объектов: 0
TmsPaletteShape Неосвобождено: 0 Максимально распределено: 5
TmsPaletteShapeCreator Неосвобождено: 0 Максимально распределено: 1
TmsUpArrow Неосвобождено: 0 Максимально распределено: 1
TmsDashDotLine Неосвобождено: 0 Максимально распределено: 164
TmsLine Неосвобождено: 0 Максимально распределено: 278
TmsRectangle Неосвобождено: 0 Максимально распределено: 144
TmsCircle Неосвобождено: 0 Максимально распределено: 908
TmsLineWithArrow Неосвобождено: 0 Максимально распределено: 309
TmsDiagrammsController Неосвобождено: 0 Максимально распределено: 1
TmsStringList Неосвобождено: 0 Максимально распределено: 3
TmsCompletedShapeCreator Неосвобождено: 0 Максимально распределено: 2
TmsRoundedRectangle Неосвобождено: 0 Максимально распределено: 434
TmsTriangleDirectionRight Неосвобождено: 0 Максимально распределено: 5
TmsGreenCircle Неосвобождено: 0 Максимально распределено: 850
TmsSmallTriangle Неосвобождено: 0 Максимально распределено: 761
TmsShapeCreator Неосвобождено: 0 Максимально распределено: 1
TmsDashLine Неосвобождено: 0 Максимально распределено: 868
TmsGreenRectangle Неосвобождено: 0 Максимально распределено: 759
TmsDiagramm Неосвобождено: 0 Максимально распределено: 910
TmsDownArrow Неосвобождено: 0 Максимально распределено: 1
TmsDotLine Неосвобождено: 0 Максимально распределено: 274
TmsDiagramms Неосвобождено: 0 Максимально распределено: 3
TmsDiagrammsHolder Неосвобождено: 0 Максимально распределено: 18
TmsPointCircle Неосвобождено: 0 Максимально распределено: 717
TmsUseCaseLikeEllipse Неосвобождено: 0 Максимально распределено: 397
TmsBlackTriangle Неосвобождено: 0 Максимально распределено: 43
TmsRedRectangle Неосвобождено: 0 Максимально распределено: 139
TmsMoverIcon Неосвобождено: 0 Максимально распределено: 220
TmsTriangle Неосвобождено: 0 Максимально распределено: 437

Ну и самое главное, часть кода мы покрыли тестами. На сегодняшний день их 174.



При этом на тестах сохранения в PNG рождаются такие рисунки:
image image image

Размер “эталона” проверки рисований красного круга: 1048x2049 пикселей. Размер файла 1.7 MB.
Однако о деталях дальше.

Начнем в обратном порядке.

Тесты.



Первым делом подключим DUnit к проекту. Для этого добавим одну строчку в проект, после чего он выглядит так:
program MindStream;

uses
  FMX.Forms,
  …
  ;

begin
  Application.Initialize;
  Application.CreateForm(TfmMain, fmMain);
  // Подключаем свой GUI_Runner, который в свою очередь найдет все зарегестрированные тесты
  u_fmGUITestRunner.RunRegisteredTestsModeless;
  Application.Run;
end.

Теперь проверим работоспособность DUnit с помощью 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.

Следующим шагом добавим первые тесты, однако сразу разделим их по классификации:
интеграционные;
модульные.

Начнем с интеграционных. Первым тестом узнаем, все ли наши фигуры зарегистрированы:
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.

Ещё два подобных теста напишем для проверки количества фигур, которые нам необходимы:
...
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;

Теперь перейдем к модульным.
Для начала напишем базовый класс модульного теста.
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

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));
      // - берём результаты от ПРЕДЫДУЩИХ тестов, НЕКОШЕРНО с точки зрения TDD
      // НО! Чертовски эффективно.
      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;

Ну а теперь кратко о том, как это все работает.
Хоть наш класс, хоть и является абстрактным, однако вся логика спрятана именно тут. Он унаследован от TTestCase из DUnit, а значит, при желании, любой потомок сможет быть зарегистрирован для тестирования, реализуя, благодаря наследованию, уникальные настройки, которые не входят в контекст.

Сам смыл тестирования (как мы его видим; и это совсем не TDD) мы очень детально описали на примере тестирования простейшего калькулятора в нашем блоге.

В двух словах — использование тестирования с помощью эталонов предполагает сохранение значений и результата теста в файл, который мы затем сравниваем с эталонным. Если файлы не совпадают, то тест “провалился”. Тут возникает вопрос: откуда мы возьмем эталонный файл? И здесь у нас два варианта: либо мы его создадим руками, либо (как поступил я) если эталона не существует, то мы создаем его автоматически на основе файла результата тестирования, так как допускаем (проверяем вручную по старинке на глаз), что тесты у нас заведомо правильные.

Как заметил внимательный читатель, в классе вовсю используются лямбды и анонимные методы. Это, для нас, один из способов поддерживать принцип DRY, там, где этого недостаточно, мы используем — наследование. Не скажу, кто из них главный (скорее, важна комбинация и умение распознать, где какой прием лучше), но могу точно сказать — мы придерживаемся принципа на 95%. Остальные 5, скорее, лень или здравый смысл.

Перестану мучить теорией и покажу классы потомки:
  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;

Как видим, изменилось не много. По сути, мы просто сказали, как изменить имя результата. Сделано так потому, что мы будем использовать базовый класс для всех тестов. Однако, лишь следующие будут проверять сериализацию, другой класс будет “результировать” в *.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));
    // - берём результаты от ПРЕДЫДУЩИХ тестов, НЕКОШЕРНО с точки зрения TDD
    // НО! Чертовски эффективно.
    l_FileName := TestResultsFileName;
    l_Diagramms.SaveTo(l_FileName);
    CheckFileWithEtalon(l_FileName);
  finally
    l_Diagramms := nil;
  end; // try..finally
end;


Тест фигур.
  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;


Про тест сохранения в png, единственная важная строчка тут:
function TTestSaveToPNG.TestResultsFileName: String;
const
  c_PNG = 'PNG\';
begin
  // Так как мы с коллегой работаем на разных мониторах, соответственно, с разными расширениями, мы тут немножко читим. Опять же, учитывая здравый смысл. 
  Result := MakeFileName(Name, c_PNG + ComputerName + '\');
end;


Полный текст модуля:
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));
      // - берём результаты от ПРЕДЫДУЩИХ тестов, НЕКОШЕРНО с точки зрения TDD
      // НО! Чертовски эффективно.
      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));
    // - берём результаты от ПРЕДЫДУЩИХ тестов, НЕКОШЕРНО с точки зрения TDD
    // НО! Чертовски эффективно.
    l_FileName := TestResultsFileName;
    l_Diagramms.SaveTo(l_FileName);
    CheckFileWithEtalon(l_FileName);
  finally
    l_Diagramms := nil;
  end; // try..finally
end;

end.


Класс для теста сохранения в *.png выглядит так:
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.

Опять же, внимательный читатель, который работал/работает с DUnit, заметит, что нет регистрации классов тестирования. А значит, прикрути мы их сейчас к проекту, ничего не случится.

Введём новый класс, который будет собой представлять “набор тестов” или, как его назвала команда DUnit, TestSuite.

Вот она — «наша особая магия».

Мы унаследуем новый класс от TestSuite. При этом “сделаем” каждый класс уникальным.
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 := 'Диаграмма ' + 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

// Вот где регистрация !!!
RegisterTest(TmsShapesTest.Create);
RegisterTest(TmsDiagrammsTest.Create);
RegisterTest(TmsDiagrammsToPNGTest.Create);

end.

Наибольшую ценность в объяснении требует лишь один метод. Разберем его по строчкам.
procedure TmsParametrizedShapeTestSuite.AddTests(TestClass: TTestCaseClass);
begin
  // Контракт
  Assert(TestClass.InheritsFrom(TmsShapeTestPrim));

  // Задаем Random
  RandSeed := 10;
  // Создаем тесты с учетом контекста тестирования
  TmsShapeTestPrim.CheckShapes(
    procedure(aShapeClass: RmsShape)
    var
      l_Method: TRttiMethod;
      l_DiagrammName: String;
      l_Seed: Integer;
      l_ShapesCount: Integer;
    begin
      // Создаем “уникальный” контекст! Важно!

      // Задаем Random
      l_Seed := Random(High(l_Seed));
      // Формируем уникальное имя для диаграммы
      l_DiagrammName := 'Диаграмма ' + IntToStr(Random(10));
      // Задаем погрешность количества фигур
      l_ShapesCount := Random(1000) + 1;
      // Применяем новый RTTI. Для решения нужных нам проблем (всё вот так просто :), ну и далее вызываем нужный нам тест, с нужными нам параметрами (контекстом))
      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;

Спасибо всем кто дочитал, как всегда, замечания и комментарии — приветствуются.

Repository
Белых Игорь @instigator21
карма
11,2
рейтинг 0,0
Пользователь
Самое читаемое Разработка

Комментарии (10)

  • +2
    У меня огромная просьба к минусующим. Напишите плз почему. Можно в личку. Я реально не понимаю за что, и почему…
    • +2
      Мои предположения:
      1. Дельфинов выжило мало, ценителей FMX тем более днём с огнём не сыщешь, поэтому плюсов с гулькин нос.
      2. 8 злобных троллей стабильно поставляют минусы.
      3. Простыни кода не располагают к чтению.
    • +2
      Я воздержался, но хочу заметить следующее.
      У статьи очень хромает полезность. У софта тоже очень хромает полезность.

      Если вы хотели софтом поделиться, то давайте посмотрим на софт:
      Из скриншотов видно, что все, что я могу — накидать шейпов, соединить прямыми стрелочками. В шейпы написать ничего нельзя. Стрелочки в колена не изогнуть. Да черт с ним. Даже цвет и размер шейпов не поменять. То есть функционал софта стремится к нулю. Не в качестве рекламы, у меня последняя статья ( habrahabr.ru/post/246895/ ), я в ней делился софтом написанным за 2-3 дня. Сравните разницу.

      Ок, вы хотели поделится не софтом, а каким-то другим материалом? Вот только я не пойму каким.
      Сначала я вижу проект, который тянет отсилы на сутки разработки в одиночку (без юнит тестов). Ок, основное время заняло написание тестов. Сразу видно, что это ваши первые тесты, потому что рисовать в пнг, а потом сравнивать это бред. Вы такие тесты очень скоро исключите из тестирования, потому что замахаетесь gold файлы апдейтить. Ну и ваш текст по тестам составляет простыни кода. Зачем? Заливаете в репозиторий, кому надо — посмотрит. А вот чтобы захотелось посмотреть — нужно дать хороший пример, а ваш пример вначале статьи никак не располагает.
      • 0
        Расскажите плз, ваш алгоритм для тестирования функции сохранения Canvas в *.png
        Без иронии, просто интересно
        • 0
          Такое обычно не тестируют в TDD. TDD должно тестировать, что шейп находится в нужном месте, нужного размера, а не результат рендера на канву.
          • 0
            Имел в прошлом опыт внедрения SCRUM. При отсутствии TDD и регрессивных тестов. Вернее у нас были test case и 2 тестера. Мы каждый спринт лажали при регрессивном тестировании. Поэтому их место в паке Regressive. Спасибо.
    • 0
      Вероятно потому что «проект» выглядит непрезентабельно, кода по самому проекту «с гулькин нос», а тесты (код которых тут просто адова стена) многие программисты воспринимают как «программирование ради программирования».
    • +2
      Думаю проблема в том, что кода дофига, он не спрятан под спойлеры. Текст очень сухой и непонятно о чем в итоге.
      Сам пишу на делфи, начал читать, но не осилил.

      От оценки статьи воздержался.
  • 0
    Спасибо всем. Вы, во многом правы.
  • 0
    Даа, простыня из кода без спойлера большая, не осилил статью.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.