Tuesday, March 16, 2021

Unittests in Bold. Is it possible ?

 I started to explore interfaces. One of the reasons I want to know more about interfaces is that I want to use unittests. Interfaces are a key part that makes it easier to make the code more testable.
I worked with Delphi for a long time and I know the concept behind interfaces but never really created new ones and used them. I believe one reason for this is that Bold generates code for classes in the model in one file businessclasses.pas. businessclasses.pas is a hotspot. It must be included whenever you want to reference a class or member in the model.

The structure of this big file is this:

(*****************************************)
(*      This file is autogenerated       *)
(*   Any manual changes will be LOST!    *)(*****************************************)
(* Generated 09.02.2021 17:51:02         *)(*****************************************)
(* This file should be stored in the     *)
(* same directory as the form/datamodule *)
(* with the corresponding model          *)(*****************************************)

unit BusinessClasses;

{$INCLUDE BusinessClasses_Interface.inc}

{$INCLUDE AmObject.inc}
{$INCLUDE Quantity.inc}
// and 80 other include files

procedure TBusinessClassesRootList.Add(NewObject: TBusinessClassesRoot);
begin
  if Assigned(NewObject) then
    AddElement(NewObject);
end;

function TBusinessClassesRootList.IndexOf(anObject: TBusinessClassesRoot): Integer;
begin
  result := IndexOfElement(anObject);
end;

// And example of code for class TPlanMission in model.
// The link TPlanMission.StateInProcess can be read and changed
// The model contains thousands of these

function TPlanMission._Get_M_stateInProcess: TBoldObjectReference;
begin
  assert(ValidateMember('TPlanMission', 'stateInProcess', 194, TBoldObjectReference));
  Result := TBoldObjectReference(BoldMembers[194]);
end;

function TPlanMission._GetstateInProcess: TVehiclePool;
begin
  Result := TVehiclePool(M_stateInProcess.BoldObject);
  if (assigned(Result) and not (Result is TVehiclePool)) then
    Assert(false, SysUtils.format(BoldMemberAssertInvalidObjectType, [ClassName, 'stateInProcess', Result.ClassName, 'TVehiclePool']));
end;

procedure TPlanMission._SetstateInProcess(const value: TVehiclePool);
begin
  M_stateInProcess.BoldObject := value;
end;


BusinessClasses_Interface.inc contains actual class definitions.

interface

uses
  AttracsAttributes,
  BoldAttributes,
  Classes,
  SysUtils;

type
  TBusinessClassesRoot = class;
  TBusinessClassesRootList = class;
  TAmObject = class;
  TAmObjectList = class;

// About 900 more forward declarations of classes

  TBusinessClassesRoot = class(TBoldObject)
  private
  protected
  public
  end;

// To make it easier to read I only left Created property in AmObject and a override method in Bold CompleteOverride

  TAmObject = class(TBusinessClassesRoot)
  private
    function _Get_M_Created: TBADateTime;
    function _GetCreated: TDateTime;
    procedure _SetCreated(const NewValue: TDateTime);
  protected
    procedure CompleteCreate; override;
  public
    property M_Created: TBADateTime read _Get_M_Created;
    property Created: TDateTime read _GetCreated write _SetCreated;
  end;

// Definitions of other 479 classes follows here

implementation

uses
  {$INCLUDE Attracs_Implementation_Uses.inc} ,

So the result can be big, really big.
Attracs have 480 classes. 
BusinessClasses.pas has 88902 lines
BusinessClasses_Interface.inc has 37480 lines
Attracs_Implementation_Uses.inc has only 90 lines

Those files are generated from the model. Only inc-files like AmObject.inc, Quantuty.inc, etc with the actual code for methods can be manually edited.

So now when we know about the structure the BIG question is. How can I make unit tests for classes in businessclasses.pas ? This is the main reason I wrote this post to get some input from other developers.
I think the main problem with businessclasses.pas is the size and it is generated code.
Generally, a developer wants to split it so each unit only contains one or a couple of classes. These make the code more flexible. Attracs_Implementation_Uses.inc also include other units not related to model.

Lets take an example. Attracs_Implementation_Uses.inc contains ParcelTrackingHandling.

This is the unit

unit ParcelTrackingHandling;

interface

uses
  Classes,
  SysUtils,
  BusinessClasses,
  BusinessInterfaces;

type
  TMediaHandle = class(TInterfacedObject)
  private
    fSendmedias: TStringList;
    function SendLinkTo(aContactMedia: TContactMedia; aParcel: TParcel): Boolean;
    function SendMail(const aEmailAddress: string; aParcel: TParcel): Boolean;
    function SendSms(const aMobileNumber: string; aParcel: TParcel): Boolean;
    function ExternalLinkPossibleToSend(aParcel: TParcel): Boolean;
    function GetTrackingSubject(aParcel: TParcel): string;
    procedure MayAddRole(aResults: TStringList; aContactMedia: TContactMedia);
  public
    constructor Create;
    destructor Destroy; override;
    function GetReceivingParty(aParcel: TParcel): TDepartment;
    function SendExternalLink(aParcel: TParcel; aResults: TStringList): Boolean;
  end;

I could make an interface for this in a new unit

unit BusinessInterfaces;

interface

uses
  Classes,
  BusinessClasses;

type
  ITracking = interface
  ['{309FF866-56EC-405D-8168-D03DB8BBB8F2}']
    function GetReceivingParty(aParcel: TParcel): TDepartment;
    function SendExternalLink(aParcel: TParcel; aResults: TStringList = nil): Boolean;
  end;

implementation

end.

But unfortunately this require BusinessClasses in the uses as TParcel that is a class in the model is used as parameter. Any idea how to avoid this chicken and egg situation ?

Input and comments are appreciated 😃