tiOPF |
Free, Open Source Object Persistence Framework for Free Pascal & Delphi |
unit Person_TST;Note we use TtiTestCase as the parent for TPersonTestCase as we will be accessing some specific tiOPF test methods.
interface
uses
tiTestFramework;
type
TPersonTestCase = class(TtiTestCase)
published
procedure PersonList_Read;
end;
implementation
uses
TestFramework;
{ TPersonTestCase }
procedure TPersonTestCase.PersonList_Read;
begin
Assert(False, 'Under construction');
end;
initialization
RegisterTest(TPersonTestCase.Suite);
end.
program DUnitAdrsBookGUI;Executing DUnitAdrsBookGUI will bring up the following main form showing three new buttons ‘Leak detection’, ‘Warnings’, ‘Tests without Check called’ and ‘Summary level testing’:
uses
FastMM4,
GUITestRunner,
Person_TST in 'Person_TST.pas',
Person_BOM in '..\BOM\Person_BOM.pas';
{$R *.res}
begin
GUITestRunner.RunRegisteredTests;
end.
Create Database 'Adrs.fdb' user 'SYSDBA' password 'masterkey';Connecting a tiOPF application to a database is a two step process:
connect "Adrs.fdb" user "SYSDBA" password "masterkey" ;
create table person
(
oid
varchar(36) not null,
first_name varchar(60),
last_name varchar(60),
title varchar(10),
initials varchar(10),
);
alter table person add
constraint person_pk
primary key (oid);
usesRun the unit test app again to confirm the database connection works.
…
tiQueryIBX,
tiOPFManager,
…
GTIOPFManager.ConnectDatabase('/path/to/database/adrs.fdb', 'SYSDBA', 'masterkey');
usesIn TPersonList, the Items[] property and access methods are recast to accept and return TPerson instances. This requires some effort upfront that will be rewarded when you start using the TPersonList in code.
tiObject;
type
TPerson = class;
TPersonList = class;
TPersonList = class(TtiObjectList)
protected
function GetItems(i: integer): TPerson; reintroduce;
procedure SetItems(i: integer; const AValue: TPerson); reintroduce;
public
property Items[i:integer]: TPerson read GetItems write SetItems;
procedure Add(const AObject : TPerson); reintroduce;
end;
TPerson = class(TtiObject)
protected
function GetParent: TPersonList; reintroduce;
public
property Parent: TPersonList read GetParent;
end;
TPerson = class(TtiObject)
…
published
property Title: string read FTitle write FTitle;
property FirstName: string read FFirstName write FFirstName;
property LastName: string read FLastName write FLastName;
property Initials: string read FInitials write FInitials;
end;
TPersonList = class(TtiObjectList)Rough out the TPersonTestCase with the following code:
…
public
procedure Read; override;
end;
implementation
procedure TPersonList.Read;
begin
inherited;
end;
Run the unit tests and you will see the dialog shown below:
implementation
uses
Person_BOM,
TestFramework,
tiDialogs;
procedure TPersonTestCase.PersonList_Read;
var
LList: TPersonList;
begin
LList := TPersonList.Create;
try
LList.Read;
// tiShowString lets us confirm persistence is working, before
// writing the details of the test
tiShowString(LList.AsDebugString);
finally
LList.Free;
end;
end;
GTIOPFManager is the globally available, single instance of TtiOPFManager, which maintains a list of registered persistence layers, databases and mapping relationships.
GTIOPFManager.ClassDBMappingMgr.RegisterMapping(
<ClassName>,
<TableName>,
<PropertyName>,
<Database field name>,
[<Optional relationship info>]);
To see this in action, add the following code to Person.pas’s Initialization section:
GTIOPFManager.ClassDBMappingMgr.RegisterCollection(
<List - TtiObjectList descendant>,
<Item – TtiObject descendant>);
Run the unit tests again and the test list will still be empty because we have not put any records into the database.
implementation
uses
tiOPFManager,
tiAutoMap;
…
initialization
GTIOPFManager.ClassDBMappingMgr.RegisterMapping(TPerson, 'person', 'oid', 'oid', [pktDB]);
GTIOPFManager.ClassDBMappingMgr.RegisterMapping(TPerson, 'person', 'FirstName','first_name');
GTIOPFManager.ClassDBMappingMgr.RegisterMapping(TPerson, 'person', 'LastName', 'last_name');
GTIOPFManager.ClassDBMappingMgr.RegisterMapping(TPerson, 'person', 'Initials', 'initials');
GTIOPFManager.ClassDBMappingMgr.RegisterMapping(TPerson, 'person', 'Title', 'title');
GTIOPFManager.ClassDBMappingMgr.RegisterCollection(TPersonList, TPerson);
Feature |
Automapping |
DB independent |
Hard coded |
Flat files |
Yes |
Yes |
No |
Use stored procedures |
No |
No |
Yes |
Swap database easilly |
Yes |
Yes |
Perhaps |
Control over SQL |
No |
No |
Complete |
Control over performance |
No |
No |
Complete |
Learning curve |
Easy |
Moderate |
Hard |
Run the unit tests again and you will see a single object has been added to the list:
procedure TPersonTestCase.PersonList_Read;
var
LList: TPersonList;
begin
GTIOPFManager.ExecSQL(
'insert into person ' +
'(OID, FIRST_NAME, LAST_NAME, TITLE) ' +
'values ' +
'(''1000'', ''Edna'', ''Everage'', ''Dame'') ');
LList := TPersonList.Create;
try
LList.Read;
tiShowString(LList.AsDebugString)
finally
LList.Free;
end;
end;
LList := TPersonList.Create;This checks there is exactly one object returned from the database, and confirms it’s properties have been set as expected.
try
LList.Read;
CheckEquals(1, LList.Count);
CheckEquals('1000', LList.Items[0].OID.AsString);
CheckEquals('Edna', LList.Items[0].FirstName);
CheckEquals('Everage', LList.Items[0].LastName);
CheckEquals('Dame', LList.Items[0].Title);
finally
LList.Free;
end;
procedure TPersonTestCase.PersonList_Read;DeleteRow takes two parameters:
var
LList: TPersonList;
begin
GTIOPFManager.DeleteRow('person', nil);
GTIOPFManager.ExecSQL(
'insert into person ' +
'(OID, FIRST_NAME, LAST_NAME, TITLE) ' +
'values ' +
'(''1000'', ''Edna'', ''Everage'', ''Dame'') ');
Compile and run the unit tests.
procedure TPersonTestCase.PersonList_Create;
var
LList: TPersonList;
LItem: TPerson;
begin
GTIOPFManager.DeleteRow('person', nil);
LItem:= TPerson.Create;
try
LItem.OID.AsString:= '1000';
LItem.FirstName:= 'Edna';
LItem.LastName:= 'Everage';
LItem.Title:= 'Dame';
LItem.Dirty:= True; // <<== Tell the OPF this object is to be saved
LItem.Save;
finally
LItem.Free;
end;
LList := TPersonList.Create;
try
LList.Read;
CheckEquals(1, LList.Count);
CheckEquals('1000', LList.Items[0].OID.AsString);
CheckEquals('Edna', LList.Items[0].FirstName);
CheckEquals('Everage', LList.Items[0].LastName);
CheckEquals('Dame', LList.Items[0].Title);
finally
LList.Free;
end;
end;
interfaceThe TPersonTestCase owns an instance of TPersonSetup. In a larger application, TPersonSetup may be owned by an abstract TMyAppTestCase, or it may be added to each TestCase as required.
uses
tiTestFramework,
tiTestSetup,
Person_BOM;
type
TPersonSetup = class(TtiTestSetup)
public
procedure PersonAssign(const APerson: TPerson; const AOID: string);
function PersonCreate(const AOID: string): TPerson;
procedure PersonInsert(const AOID: string);
procedure PersonCheck(const APerson: TPerson; const AOID: string);
end;
TPersonTestCase = class(TtiTestCase)
private
FPersonSetup: TPersonSetup;
protected
procedure SetupOnce; override; // <<== SetUpOnce, new to DUnit2 will
procedure TearDownOnce; override; //<<== be called once for each test run.
…
procedure TPersonTestCase.SetupOnce;
begin
inherited;
FPersonSetup:= TPersonSetup.Create(Self);
end;
procedure TPersonTestCase.TearDownOnce;
begin
FPersonSetup.Free;
inherited;
end;
tvToStr() will ‘Increment’ AOID so each property of TPerson is assigned a unique value. These properties will be meaningless strings based on the value of AOID, but they will be unique which will test our persistence code does not cross wire any mappings between fields and the database.
procedure TPersonSetup.PersonAssign(const APerson: TPerson; const AOID: string);
begin
APerson.FirstName:= tvToStr(AOID, 1);
APerson.LastName:= tvToStr(AOID, 2);
APerson.Title:= tvToStr(AOID, 3);
APerson.Initials:= tvToStr(AOID, 4);
end;
Note that the OID is set in PersonCreate()
function TPersonSetup.PersonCreate(const AOID: string): TPerson;
begin
result:= TPerson.Create;
result.OID:= AOID;
PersonAssign(result, AOID);
end;
procedure TPersonSetup.PersonInsert(const AOID: string);PersonCreate() is used to seed a fresh instance of TPerson.
var
LPerson: TPerson;
LParams: TtiQueryParams;
begin
LPerson:= nil;
LParams:= nil;
try
LPerson:= PersonCreate(AOID);
LParams:= TtiQueryParams.Create;
LParams.SetValueAsString('oid', LPerson.OID.AsString);
LParams.SetValueAsString('first_name', LPerson.FirstName);
LParams.SetValueAsString('last_name', LPerson.LastName);
LParams.SetValueAsString('title', LPerson.Title);
LParams.SetValueAsString('initials', LPerson.Initials);
GTIOPFManager.InsertRow('person', LParams);
finally
LPerson.Free;
LParams.Free;
end;
end;
The CheckEquals() methods are being called from within a TtiTestCaseSetup descendant, so must be made by the referenced TC (TestCase)
procedure TPersonSetup.PersonCheck(const APerson: TPerson; const AOID: string);
var
LPerson: TPerson;
begin
LPerson:= PersonCreate(AOID);
try
TC.CheckEquals(LPerson.FirstName, APerson.FirstName);
TC.CheckEquals(LPerson.LastName, APerson.LastName);
TC.CheckEquals(LPerson.Title, APerson.Title);
TC.CheckEquals(LPerson.Initials, APerson.Initials);
finally
LPerson.Free;
end;
end;
Person_Read can be modified to reference the PersonSetup methods as shown in the code below:
procedure TPersonTestCase.SetUp;
begin
inherited;
GTIOPFManager.DeleteRow('person', nil);
GTIOPFManager.DeleteRow('adrs', nil);
GTIOPFManager.DeleteRow('eadrs', nil);
GTIOPFManager.DeleteRow('adrs_type', nil);
GTIOPFManager.DeleteRow('eadrs_type', nil);
end;
And now Person_Create can be tidied up:
procedure TPersonTestCase.PersonList_Read;
var
LList: TPersonList;
begin
PersonSetup.PersonInsert(COIDPerson1);
LList := TPersonList.Create;
try
LList.Read;
CheckEquals(1, LList.Count);
PersonSetup.PersonCheck(LList.Items[0], COIDPerson1);
CheckEquals(COIDPerson1, LList.Items[0]);
finally
LList.Free;
end;
end;
procedure TPersonTestCase.PersonList_Create;
var
LList: TPersonList;
LItem: TPerson;
begin
LItem:= PersonSetup.PersonCreate(COIDPerson1);
try
LItem.Dirty:= True;
LItem.Save;
finally
LItem.Free;
end;
LList := TPersonList.Create;
try
LList.Read;
PersonSetup.PersonCheck(LList.Items[0], COIDPerson1);
CheckEquals(COIDPerson1, LList.Items[0]);
finally
LList.Free;
end;
end;
procedure TPersonTestCase.Person_Update;
var
LList: TPersonList;
begin
PersonSetup.PersonInsert(COIDPerson1); // <<== Insert Person #1
LList := TPersonList.Create;
try
LList.Read;
PersonSetup.PersonCheck(LList.Items[0], COIDPerson1);// <<== Check it's Person #1
PersonSetup.PersonAssign(LList.Items[0], COIDPerson2);// <<== Assign to Person #2
LList.Items[0].Dirty:= True;
LList.Items[0].Save;
finally
LList.Free;
end;
LList := TPersonList.Create;
try
LList.Read;
PersonSetup.PersonCheck(LList.Items[0], COIDPerson2); // <<== Check it's Person #2
finally
LList.Free;
end;
end;
procedure TPersonTestCase.Person_Delete;
var
LList: TPersonList;
begin
PersonSetup.PersonInsert(COIDPerson1);
LList := TPersonList.Create;
try
LList.Read;
LList.Items[0].Deleted:= True; // <<== This object is to be deleted
LList.Items[0].Save;
finally
LList.Free;
end;
LList := TPersonList.Create;
try
LList.Read;
CheckEquals(0, LList.Count); //<<== Should have been deleted
finally
LList.Free;
end;
end;
unit FMain;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Person_BOM;
type
TFormMain = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FPersonList: TPersonList;
public
{ Public declarations }
end;
var
FormMain: TFormMain;
implementation
uses
tiOPFManager,
tiQueryIBX;
{$R *.dfm}
procedure TFormMain.FormCreate(Sender: TObject);
begin
GTIOPFManager.ConnectDatabase('adrs', 'adrs.fdb', 'SYSDBA', 'masterkey', '', '');
FPersonList:= TPersonList.Create;
FPersonList.Read;
end;
procedure TFormMain.FormDestroy(Sender: TObject);
begin
FPersonList.Free;
end;
procedure TFormMain.FormCreate(Sender: TObject);
begin
GTIOPFManager.ConnectDatabase('adrs', 'adrs.fdb', 'SYSDBA', 'masterkey', '', '');
FPersonList:= TPersonList.Create;
FPersonList.Read;
LV.AddColumn('Title', vttkString, 'Title', 100);
LV.AddColumn('FirstName', vttkString, 'First Name', 200);
LV.AddColumn('LastName', vttkString, 'Last Name', 200);
LV.Data:= FPersonList;
end;
Set the TtiVTListView’s VisibleButtons property to [tiLVBtnVisEdit,tiLVBtnVisNew,tiLVBtnVisDelete] These buttons provide a quick path to coding a popup modal form:
procedure TFormMain.LVItemInsert(In OnItemDelete, add the following code:
pVT: TtiCustomVirtualTree; AData: TtiObject; AItem: PVirtualNode);
var
LData: TPerson;
begin
LData:= TPerson.CreateNew;
FPersonList.Add(LData);
LData.LastName:= 'test';
LData.FirstName:= 'test';
LData.Save;
LV.Refresh(LData);
end;
procedure TFormMain.LVItemDelete(Compile and run the application and test Insert and Delete work.
pVT: TtiCustomVirtualTree; AData: TtiObject; AItem: PVirtualNode);\
begin
if tiObjectConfirmDelete(AData) then // Add tiGUIUtils to the uses
begin
AData.Deleted:= True;
(AData as TPerson).Save;
FPersonList.FreeDeleted;
LV.Refresh;
end;
end;
procedure TFormPersonEdit.SetData(const AValue: TtiObject);Inherited is called so TFormTIPerEditDialog .SetData() can clone the instance being edited. The cloned copy is edited and compared against the copy that has been passed. This is used to provide basic undo functionality, as well as enable / disable the OK button. The data instance that was passed to the form’s Execute method can be accessed via the Data property. The cloned instance can be accessed via the DataBuffer property.
begin
inherited;
paeTitle.LinkToData(DataBuffer, 'Title');
paeFirstName.LinkToData(DataBuffer, 'FirstName');
paeLastName.LinkToData(DataBuffer, 'LastName');
end;
function TFormPersonEdit.FormIsValid: boolean;FormIsValid depends on some functionality in TPerson.IsValid that we have not yet implemented.
var
LS: string;
begin
result:= DataBuffer.IsValid(LS);
lblErrors.Caption:= LS;
lblErrors.Visible:= LS <> '';
end;
function TPerson.IsValid(const AErrors: TtiObjectErrors): boolean;With IsValid implemented, the edit form will show an error message, and disable the OK button as shown below:
begin
result:= inherited IsValid(AErrors);
if (FirstName = '') and (LastName = '') then
AErrors.AddError(CErrorPersonNameNotAssigned);
// ToDo: Add code to check field lengths will fit in the DB
result:= AErrors.Count = 0;
end;
var
LItem: TPerson;
LErrors: TtiObjectErrors;
begin
LErrors:= nil;
LItem:= nil;
try
LErrors:= TtiObjectErrors.Create;
LItem:= PersonSetup.PersonCreate(cOIDPerson1);
Check(LItem.IsValid(LErrors));
CheckEquals(0, LErrors.Count);
LItem.Title:= '';
Check(LItem.IsValid(LErrors));
CheckEquals(0, LErrors.Count);
LItem.Initials:= '';
Check(LItem.IsValid(LErrors));
CheckEquals(0, LErrors.Count);
LItem.FirstName:= '';
Check(LItem.IsValid(LErrors));
CheckEquals(0, LErrors.Count);
LItem.LastName:= '';
Check(not LItem.IsValid(LErrors));
CheckEquals(1, LErrors.Count);
CheckEquals(CErrorPersonNameNotAssigned, LErrors.Items[0].ErrorMessage);
finally
LErrors.Free;
LItem.Free;
end;
end;
result:= Title;GetCaption must also be unit tested (which is trivial so not shown here).
if FirstName <> '' then
result:= result + ' ' + FirstName;
if LastName <> '' then
result:= result + ' ' + LastName;
end;
procedure TPersonTestCase.Person_Equals;TestTIObjectEquals is implemented in TtiTestCase and is used to test Equals by changing one property at the time as shown in the following code fragment:
var
LItem1: TPerson;
LItem2: TPerson;
begin
LItem1:= nil;
LItem2:= nil;
try
LItem1:= PersonSetup.PersonCreate(COIDPerson1);
LItem2:= PersonSetup.PersonCreate(COIDPerson1);
TestTIObjectEquals(LItem1, LItem2, 'LastName');
TestTIObjectEquals(LItem1, LItem2, 'FirstName');
TestTIObjectEquals(LItem1, LItem2, 'Title');
TestTIObjectEquals(LItem1, LItem2, 'Initials');
finally
LItem1.Free;
LItem2.Free;
end;
end;
procedure TtiTestCase.TestTIObjectEquals(
const AObj1, AObj2: TtiObject; const APropName: String);
var
LSavedStr: string;
begin
Check(AObj1.Equals(AObj2), 'Expected equality');
LSavedStr:= AObj2.PropValue[APropName];
AObj2.PropValue[APropName]:= AObj2.PropValue[APropName] + 'A';
Check(not AObj1.Equals(AObj2), 'Expected inequality');
AObj2.PropValue[APropName]:=LSavedStr;
Check(AObj1.Equals(AObj2), 'Expected equality');
end;
procedure TTestPerson.Person_Assign;
var
LFrom: TPerson;
LTo : TPerson;
begin
LFrom:= PersonSetup.PersonCreate(COIDPerson1);
try
LTo:= TPerson.Create;
try
LTo.Assign(LFrom);
PersonSetup.PersonCheck(LTo, COIDPerson1);
finally
LTo.Free;
end;
finally
LFrom.Free;
end;
end;