Unit Testing With TestComplete
Eric Holton
© 2002 Holton Integration Systems
Directly testing software at a small section of the source code (unit, component, and/or function) is referred to as "Unit Testing". Unit testing checks that the code is following the design, that the input/output behave as expect, and that any error conditions are generated when required.
To illustrate unit testing with TestComplete, we will build a simple object. At each stage of the object development, we will write the test to check that our object is working as expected and to according to the design specifications.
To download the entire source code of the tested application and the
TestComplete project used in this article, click here.
Getting Started
First, Let us look at what the object (unit) we are implementing will do.
Name Extractor
The Name Extractor takes the text input of an English/American proper name (i.e. "Mr Frank Borland") and extracts the following information from it:
1. Title (i.e. "Mr")
2. First Name (i.e. "Frank")
3. Middle Name (blank in sample case)
4. Last Name (i.e. "Borland")
5. Suffix (blank in sample case, could be Jr, II, PhD, DDS, etc)
From this one input (proper name), we have generated five outputs. In order to get these outputs, we need to define some business rules.
Business Rules
- The name will be broken apart in to a word list.
- Titles will come from a preset list.
- Suffix:
- Starting from the end of the word list, all words that end in a period are suffixes.
- Starting from the end of the word list, all words after the final comma are suffixes
- Any words in the preset suffix list will be suffixes. Starting from the end of the word list, check if word is in the suffix list, if not stop searching.
- Last Name:
- Last Name is the word before the comma in word list, if any.
- If there is only one word it is the last name
- If there are two words, and the second is not a suffix accord to the rules in 3, then that word is the last name, otherwise the first word is the last name.
- If there are more than two words, the last word in the list, which is not a suffix, is the Last Name.
- If there are words before the last name (as set by rule 4), then the first word is tested against the title list. If the word is in the title list, it is treated as the title. If not the Title remains empty.
- If there are any words before the last name but after the title (if any), then the first of the words is the First Name and all other words belong to the Middle Name.
- If a name has a Title that contains a vowel, but not a first name (i.e. Major Brown), the object will raise an exception.
Creating the Delphi Project
In this example, we will be building a Delphi project. So start up Delphi, and create a new project. The normal way of compiling for TestComplete is to link TCClient into the application (which makes it an 'Open Application', see TestComplete Help). Make sure that you do that. Now add a new unit to the project, save the project. In the new unit add the skeleton for the new class in the implementation section:
{$M+}
type
TNameExtractor = class
private
FFullName: string;
FTitle: string;
FFirstName: string;
FMiddleName: string;
FLastName: string;
FSuffix: string;
procedure SetFullName(const Value: string);
public
property Title: string read FTitle;
property FirstName: string read FFirstName;
property MiddleName: string read FMiddleName;
property LastName: string read FLastName;
property Suffix: string read FSuffix;
published
property FullName:string read FFullName write SetFullName;
end;
{$M-}
implementation
{ TNameExtractor }
procedure TNameExtractor.SetFullName(const Value: string);
begin
FFullName := Value;
end;
The {$M+} is so that RTTI is generated for the class.
Switch to the main form of the project, and use the unit that contains the TNameExtractor class. I used the following filenames:
Project File: UnitTestNameExtractor.dpr
Main Form: frmUnitTest.pas
Unit File: unNameExtractor.pas
Add to the form file a variable for the TNameExtractor
private
FNameExtractor: TNameExtractor;
function GetNameExtractor: TNameExtractor;
public
{ Public declarations }
published
property NameExtractor:TNameExtractor read GetNameExtractor;
Add to the form file the following FormClose Event.
procedure TfmUnitTest.FormClose(Sender: TObject; var Action: TCloseAction);
begin
NameExtractor.Free;
end;
function TfmUnitTest.GetNameExtractor: TNameExtractor;
begin
if not assigned(FNameExtractor) then
FNameExtractor := TNameExtractor.Create;
Result := FNameExtractor;
end;
Run the resulting program. Start up TestComplete, switch to the Object Browser and select the program (UnitTestNameExtractor). Select the main form of the program (fmUnitTest) from the Object tree, select the fields' tab in Object Properties pane. If everything is setup correctly, you should find the FnameExtractor field listed with an IDispatch field value.
Setting up TestComplete
Create a new project in TestComplete (We will be using DelphiScript for
this paper). And add the following routines to the main unit:
// Safety valve for accidentally pressing the run button;
procedure Main;
begin
Runner.Stop;
end;
// Start the application and setup global variables (for unit)
procedure SetupTests;
begin
Log.Message('Setup of Unit Tests', '', pmHigher, fmBold, clNavy);
TestedApps.RunAll;
p := Sys.WaitProcess('UnitTestNameExtractor', 5000);
if p.exists then
begin
w := p.fmUnitTest;
if not w.exists then
raise ('Window fmUnitTest not found');
DefineStringListClass; // Moved here to allow multiple runs of the tests
end
else
raise ('Process UnitTestNameExtractor Not Found');
end;
// Close the application
procedure FinalizeTests;
begin
w.Close;
repeat
Sys.Delay(500);
until not p.Exists;
Log.Message('Finalization of Unit Tests', '', pmHigher, fmBold, clNavy);
end;
Switch to the Form, using the Toggle Form/Unit Button for the main unit in TestComplete, select the ProjectEvents1 object on the form. Switch to the Component Inspection and select the Events tab. Double-Click on the OnLogError Event. Add the following code:
procedure ProjectEvents1_OnLogError(Sender: OleVariant; LogParams: OleVariant);
begin
p := Sys.WaitProcess('UnitTestNameExtractor', 5000);
if p.exists then
begin
p.Close; // or use p.Terminate;
repeat
Sys.Delay(500);
until not p.Exists;
end;
Runner.Stop;
end;
Create a new unit in TestComplete in which we going to add some test helper functions. Save the unit as "unUnitTestFunctions". Now add the following code:
// Code Added as Part of Setup
function NotEqualsErrorMessage(expected, actual: string; msg: string): string;
begin
if (msg <> '') then
msg := msg + ', ';
Result := Format('%sexpected: <%s> but was: <%s>', [msg, expected, actual])
end;
function EqualsErrorMessage(expected, actual: string; msg: string): string;
begin
if (msg <> '') then
msg := msg + ', ';
Result := Format('%sexpected and actual were: <%s>', [msg, expected])
end;
procedure CheckEquals(Expected,Actual,Msg);
begin
if Expected <> Actual then
raise(NotEqualsErrorMessage(Expected,Actual,Msg));
end;
procedure CheckNotEquals(Expected,Actual,Msg);
begin
if Expected = Actual then
raise(EqualsErrorMessage(Expected,Actual,Msg));
end;
procedure CheckException(Expected,Actual,Msg);
begin
if Pos(Expected,Actual) = 0 then
raise(NotEqualsErrorMessage(Expected,Actual,Msg));
end;
Add to the top of the main unit:
uses unUnitTestFunctions;
Switch to the Suites Tab (another great feature of TestComplete) and setup like the following:
|
Writing the first test
We are going to start with the idea that we have no idea how we are going
to implement the TNameExtractor object. We look over the
business rules and decide that the single word last name is the easiest
test to write. So we add a procedure to the main unit of TestComplete
as follows:
procedure TestLastName;
begin
w.activate;
w.NameExtractor.FullName := 'Borland';
CheckEquals('Borland',w.NameExtractor.LastName,'Last Name is not correct');
end;
Switch to the Suites Tab and Add as Child the new test. It should look this:

Figure 1 - Image of the Suite Tab with Child Test
Right-Click on the Test Suites and select Run All. You should receive an error, and the Test Log should look like:

Figure 2 - Image of Test Log After First Test
We now need to make the test pass by changing the Delphi source code. The simplest way is to change the setFullName procedure as below:
procedure TNameExtractor.SetFullName(const Value: string);
begin
if Value <> FFullName then
begin
FFullName := Value;
FLastName := 'Borland';
end;
end;
Run the test again, the test should pass now, but the code is not very useful. We need to extend the test in TestComplete for another last name.
procedure TestLastName;
begin
w.activate;
w.NameExtractor.FullName := 'Borland';
CheckEquals('Borland',w.NameExtractor.LastName,'Last Name is not correct');
w.NameExtractor.FullName := 'Smith';
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
end;
Run the test again, again we will receive an error in the Test Log. The quickest fix to this error is the following code:
procedure TNameExtractor.SetFullName(const Value: string);
begin
if Value <> FFullName then
begin
FFullName := Value;
FLastName := FFullName;
end;
end;
The test will run to completion with this change. Up to this point we have ignorde the other outputs, we need to check those outputs to make sure there are no side effects in our code. While we are at it we should do a check on any boundary conditions we know exist. The only boundary condition that exists currently is an empty string. So our last name test is extended to:
procedure TestLastName;
begin
w.activate;
w.NameExtractor.FullName := 'Borland';
CheckEquals('Borland',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
w.NameExtractor.FullName := 'Smith';
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
w.NameExtractor.FullName := '';
CheckEquals('',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
end;
More Tests...
So far the tests, and the class, do not amount to much. We will need a way for class to separate the input string into words. So lets write a test that takes an input string and gets the words from the string. We will be taking advantage of another feature in TestComplete for this test called custom classes. Add a new unit to TestComplete and save it as unStringListClass. The source is as follows:
function CreateStringListClass:OleVariant;
var
StringListClass:OleVariant;
begin
StringListClass := Classes.Define('StringList');
StringListClass.Field('Count');
StringListClass.Field('Size');
StringListClass.Field('Items');
StringListClass.Method('Text','unStringListClass.SetText');
Result := Classes.New('StringList');
Result.Count := 0;
Result.Size := 5;
Result.Items := CreateVariantArray(1, Result.Size);
end;
procedure SetText(Value:OleVariant);
var
tempStr:string;
tempWord:string;
spacePos:Integer;
OldArray:OleVariant;
begin
tempStr := Value;
While Length(tempStr) > 0 do
begin
count := count + 1;
if count = size then
begin
OldArray := Items;
Size := Size + 5;
VarArrayRedim(OldArray, Size);
Items := OldArray;
end;
spacePos := Pos(' ',tempStr);
if spacePos = 0 then
begin
Items[count] := tempStr;
tempStr := '';
end
else
begin
tempWord := Copy(tempStr,1,spacePos-1);
if (tempWord = '') or (tempWord = ' ') then
count := count - 1
else
Items[count] := tempWord;
Delete(tempStr,1,spacePOS);
end;
end;
end;
// Procedure for quick test of StringListClass
procedure TestStringListClass;
var
tempStrList:OleVariant;
i:integer;
begin
tempStrList := CreateStringListClass;
tempStrList.Text := 'One Two Three Four Five Six Seven Eight Nine ';
for i := 1 to tempStrList.count do
Log.Message(Format('*%s*',[tempStrList.Items[i]]));
end;
We now have a script that will compare the output of out word extractor against an expected output. Let us write the word extraction test. Switch back to the main unit in TestComplete. The procedure will set the text to a known value and return a string list with the word in it.
procedure TestExtractWords;
var
WordList:OleVariant;
i:integer;
tempStrList:OleVariant;
begin
Options.LocalVars.TestWords := 'One Two Three Four Five Six Seven Eight Nine';
w.activate;
w.NameExtractor.ExtractWords(Options.LocalVars.TestWords);
WordList := w.NameExtractor.FWords;
tempStrList := CreateStringListClass;
tempStrList.Text := Options.LocalVars.TestWords;
if tempStrList.Count <> WordList.FCount then
raise (Format('Word Count does not Match (%d <> %d)',
[tempStrList.Count,WordList.FCount]));
for i := 1 to tempStrList.Count do
if tempStrList.Items[i] <> WordList.Get(i - 1) then
raise (Format('%s <> %s in ExtractWords procedure',
[tempStrList.Items[i],WordList.Get(i - 1)]));
end;
Add the procedure to the Test Suites and run, you should receive an error.
Now add the ExtractWord function to the TNameExtractor class.
uses Classes, Windows;
TNameExtractor = class
Private
..
FWords:TStrings;
protected
procedure ExtractWords(const Value:string);
public
constructor Create;
destructor Destroy; override;
..
..
procedure TNameExtractor.SetText(const Value: string);
begin
if Value <> FFullName then
begin
FFullName := Value;
FLastName := FFullName;
ExtractWords(FFullName);
end;
end;
Note the change to the SetText procedure, this is to keep Delphi's smart linker from not including the ExtractWords procedure.
procedure TNameExtractor.ExtractWords(const Value: string);
var
P, P1: PChar;
S: string;
begin
FWords.Clear;
P := PChar(Value);
while P^ in [#1..' '] do
P := CharNext(P);
while P^ <> #0 do
begin
P1 := P;
while (P^ > ' ') do
P := CharNext(P);
SetString(S, P1, P - P1);
FWords.Add(S);
while P^ in [#1..' '] do
P := CharNext(P);
end;
end;
constructor TNameExtractor.Create;
begin
inherited;
FWords := TStringList.Create;
end;
destructor TNameExtractor.Destroy;
begin
FWords.Free;
inherited;
end;
Run the tests in TestComplete, oops we get an error (unless you have been compiling with TD32 Debug Information). We are now trying to access a private field of the TNameExtractor. TestComplete provides Debug Info Agent to do that, but it can not run unless we compile the application with TD32 Debug Information. Compile the application with Debug Information as outlined in the help file for TestComplete.
When the tests are run again, there are no errors.
The next step is to parse the input text into the component parts. We will start with the simple case of one word to make sure it does not break any of our previous tests. Switch to Delphi and add a new procedure to the TNameExtractor.
protected
procedure ExtractWords(const Value:string);
procedure ParseName;
procedure TNameExtractor.ParseName;
begin
ExtractWords(FFullName);
FLastName := FWords[0];
end;
procedure TNameExtractor.SetFullName(const Value: string);
begin
if Value <> FFullName then
begin
FFullName := Value;
ParseName;
end;
end;
Run the test in TestComplete. The application fails the tests. If we examine the code we can see that we set the FLastName to the first element of the word list, without checking that there are any elements in the first place. Modify the code as below:
procedure TNameExtractor.ParseName;
begin
ExtractWords(FFullName);
if FWords.Count > 0 then
FLastName := FWords[0];
end;
We re-run the tests, and again the tests fail but for a different reason. Since we did not reset the output strings to null before attempting to set the new values, the old value (Smith) stayed in the empty string case.
procedure TNameExtractor.ParseName;
begin
FTitle := '';
FFirstName := '';
FMiddleName := '';
FLastName := '';
FSuffix := '';
ExtractWords(FFullName);
if FWords.Count > 0 then
FLastName := FWords[0];
end;
After modifying the code, we can now run all the tests up to this point. The next step is to deal with two-word entries. Let us start with the special case of Last Name and Suffix. Using business rules of 3b and 4a, we will write a new test in TestComplete and add it to the TestSuite.
procedure TestBR3;
begin
w.activate;
w.NameExtractor.FullName := 'Smith, Esq';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('Esq',w.NameExtractor.Suffix,'Suffix Name is not correct');
end;
When we run the test we again get an error but not the one we may have expected. I expected to get the error, "Suffix Name is not correct: expected <Esq> but was <>". However the error we received was "Last Name is not correct, expected: <Smith> but was: <Smith,>". We need to strip the comma from the LastName property. Add the following code in Delphi:
protected
procedure ExtractWords(const Value:string);
procedure ParseName;
procedure StripComma(var Value:string);
procedure TNameExtractor.ParseName;
var
i:integer;
begin
FTitle := '';
FFirstName := '';
FMiddleName := '';
FLastName := '';
FSuffix := '';
ExtractWords(FFullName);
if FWords.Count > 0 then
begin
FLastName := FWords[0];
StripComma(FLastName);
end;
end;
procedure TNameExtractor.StripComma(var Value: string);
var
i: Integer;
begin
i := Length(Value);
if Value[i] = ',' then
dec(i);
Value := Copy(Value,1,i);
end;
Now when we run the test we get the expected error of "Suffix Name is not correct: expected <Esq> but was <>". If you look at the business rules, we have not addressed the idea of multiple commas in the input string. Let write a test for multiple comma, expecting the TNameExtractor to generate an exception. And we will write additional tests for the rest of business rule 3. Switch to TestComplete and add to the procedure TestBR3.
procedure TestBR3;
begin
w.activate;
//Check Comma
w.NameExtractor.FullName := 'Smith, Esq';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('Esq',w.NameExtractor.Suffix,'Suffix Name is not correct');
//Check Exception
try
w.NameExtractor.FullName := 'Smith, Esq,';
Log.Error('No Exception Generated');
except
CheckException('Too Many Commas in Full Name',ExceptionMessage,
'Invalid Exception Generated');
end;
//Check List
w.NameExtractor.FullName := 'Smith IV';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('IV',w.NameExtractor.Suffix,'Suffix Name is not correct');
end;
Let us create a procedure that encapsulates business rule 3 in Delphi.
type
ENameExtractorError = Exception;
{$M+}
TNameExtractor = class
private
FFullName: string;
FTitle: string;
FFirstName: string;
FMiddleName: string;
FLastName: string;
FSuffix: string;
FWords: TStrings;
procedure SetFullName(const Value: string);
protected
procedure ExtractWords(const Value:string);
procedure ParseName;
procedure StripComma(var Value:string);
function findSuffix:Integer;
public
constructor Create;
destructor Destroy; override;
property Title: string read FTitle;
property FirstName: string read FFirstName;
property MiddleName: string read FMiddleName;
property LastName: string read FLastName;
property Suffix: string read FSuffix;
published
property FullName: string read FFullName write SetFullName;
end;
{$M-}
implementation
resourcestring
rsSuffixList = 'DDS,CFA,CEO,CFO,Esq,CPA,MBA,PhD,MD,DC,Sr,Jr,II,III,IV';
..
function TNameExtractor.findSuffix: Integer;
var
I: Integer;
tempResult:Integer;
SuffixList:TStrings;
begin
Result := FWords.Count;
FSuffix := '';
// Business Rule 3a
for I := FWords.Count - 1 downto 0 do // Iterate
begin
if AnsiPos('.',Fwords[i]) = 0 then //No Periods
break;
Result := i;
end; // for
// Business Rule 3b
if AnsiPos(',',FFullName) <> LastDelimiter(',',FullName) then
raise ENameExtractorError.Create('Too Many Commas in Full Name');
tempResult := FWords.Count;
for I := FWords.Count - 1 downto 0 do // Iterate
begin
if AnsiPos(',',Fwords[i]) <> 0 then //Find Commas
tempResult := i + 1;
end; // for
if tempResult < Result then
Result := tempResult;
// Business Rule 3c
SuffixList := TStringList.Create;
try
SuffixList.CommaText := rsSuffixList;
tempResult := FWords.Count;
for I := FWords.Count - 1 downto 0 do // Iterate
begin
if SuffixList.IndexOf(FWords[i]) = -1 then
break;
tempResult := i;
end;
finally
SuffixList.Free;
end; // try/finally
if tempResult < Result then
Result := tempResult;
if Result = FWords.Count then
Result := -1
else
begin
for I := Result to FWords.Count - 1 do // Iterate
begin
FSuffix := Trim(Format('%s %s',[FSuffix, FWords[i]]));
end; // for
end;
end;
procedure TNameExtractor.ParseName;
var
i:integer;
iSuffix:Integer;
begin
FTitle := '';
FFirstName := '';
FMiddleName := '';
FLastName := '';
FSuffix := '';
ExtractWords(FFullName);
if FWords.Count > 0 then
begin
FLastName := FWords[0];
StripComma(FLastName);
iSuffix := FindSuffix;
end; // if
end;
If we run the tests, they will now all pass. Next, we need to find the last name. We have the tests for a single word already and for two words with suffix, so let us write tests for the remaining sub-rules in business rule 4. In TestComplete add the follow procedure to the TestSuite.
procedure TestBR4;
begin
//Check Comma (4a)
w.NameExtractor.FullName := 'Smith, PhD.';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('PhD.',w.NameExtractor.Suffix,'Suffix Name is not correct');
//Check Two Words (4c)
w.NameExtractor.FullName := 'Tom Smith';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
//Check Three Words (4d)
w.NameExtractor.FullName := 'Thomas Dale Smith';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
end;
And add/modify the following code in Delphi.
protected
procedure ExtractWords(const Value:string);
procedure ParseName;
procedure StripComma(var Value:string);
function findSuffix:Integer;
function findLastName(const Suffix:Integer):Integer;
function TNameExtractor.findLastName(const Suffix:Integer): Integer;
begin
Result := 0;
if Suffix <> - 1 then
Result := Suffix - 1
else
Result := FWords.Count - 1;
if Result < 0 then
begin
Result := 0;
FSuffix := ''; //Invalid Suffix found
end;
FLastName := FWords[Result];
StripComma(FLastName);
end;
procedure TNameExtractor.ParseName;
var
i:integer;
iSuffix:Integer;
iLastName:Integer;
begin
FTitle := '';
FFirstName := '';
FMiddleName := '';
FLastName := '';
FSuffix := '';
ExtractWords(FFullName);
if FWords.Count > 0 then
begin
iSuffix := FindSuffix;
iLastName := findLastName(iSuffix);
end; // if
end;
We are going to speed up through the remaining business rules.
Business Rule 5
TestComplete:
procedure TestBR5;
begin
w.NameExtractor.FullName := 'Tom Smith';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
w.NameExtractor.FullName := 'Mr. Smith';
CheckEquals('Mr.',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
w.NameExtractor.FullName := 'Mr. Tom Smith';
CheckEquals('Mr.',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
end;
Delphi:
protected
procedure ExtractWords(const Value:string);
procedure ParseName;
procedure StripComma(var Value:string);
function findSuffix:Integer;
function findLastName(const SuffixPos:Integer):Integer;
function findTitle(const LastNamePos:Integer):integer;
resourcestring
rsSuffixList = 'DDS,CFA,CEO,CFO,Esq,CPA,MBA,PhD,MD,DC,Sr,Jr,II,III,IV';
rsTitleList = 'Mr.,Mr,Ms.,Ms,Miss.,Miss,Dr.,Dr,Mrs.,Mrs,Fr.,Capt.,Lt.
,Gen.,President,Sister,Father,Brother,Major ';
function TNameExtractor.findTitle(const LastNamePos: Integer): integer;
var
TitleList:TStrings;
begin
FTitle := '';
Result := -1;
if LastNamePos = 0 then
exit;
TitleList := TStringList.Create;
try
TitleList.CommaText := rsTitleList;
if TitleList.Indexof(FWords[0]) <> -1 then
begin
Result := 0;
FTitle := FWords[0];
end;
finally
// free resources
TitleList.Free;
end; // try/finally
end;
procedure TNameExtractor.ParseName;
var
i:integer;
iSuffix:Integer;
iLastName:Integer;
iTitle:Integer;
begin
FTitle := '';
FFirstName := '';
FMiddleName := '';
FLastName := '';
FSuffix := '';
ExtractWords(FFullName);
if FWords.Count > 0 then
begin
iSuffix := FindSuffix;
iLastName := findLastName(iSuffix);
iTitle := findTitle(iLastName);
end; // if
end;
Business Rule 6
TestComplete:
procedure TestBR6;
begin
w.NameExtractor.FullName := 'Tom Smith';
CheckEquals('',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Tom',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
w.NameExtractor.FullName := 'Mr. Smith';
CheckEquals('Mr.',w.NameExtractor.Title,'Title is not correct');
CheckEquals('',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
w.NameExtractor.FullName := 'Mr. Tom Smith';
CheckEquals('Mr.',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Tom',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
w.NameExtractor.FullName := 'Mr. Tom Dale Smith';
CheckEquals('Mr.',w.NameExtractor.Title,'Title is not correct');
CheckEquals('Tom',w.NameExtractor.FirstName,'First Name is not correct');
CheckEquals('Dale',w.NameExtractor.MiddleName,'Middle Name is not correct');
CheckEquals('Smith',w.NameExtractor.LastName,'Last Name is not correct');
CheckEquals('',w.NameExtractor.Suffix,'Suffix Name is not correct');
end;
Delphi:
protected
procedure ExtractWords(const Value:string);
procedure ParseName;
procedure StripComma(var Value:string);
function findSuffix:Integer;
function findLastName(const SuffixPos:Integer):Integer;
function findTitle(const LastNamePos:Integer):integer;
function findFirstName(const TitlePos, LastNamePos:integer):integer;
procedure findMiddleName(const FirstNamePos,
TitlePos, LastNamePos:Integer);
function TNameExtractor.findFirstName(const TitlePos,
LastNamePos: integer): integer;
begin
Result := -1;
if LastNamePos > TitlePos + 1 then
begin
Result := TitlePos + 1;
FFirstName := FWords[Result];
end;
end;
procedure TNameExtractor.findMiddleName(const FirstNamePos,
TitlePos, LastNamePos: Integer);
var
I: Integer;
StartPos:Integer;
begin
FMiddleName := '';
StartPos := FirstNamePos + 1;
if TitlePos > FirstNamePos then
StartPos := TitlePos + 1;
for I := StartPos to LastNamePos - 1 do // Iterate
begin
FMiddleName := Trim(Format('%s %s',[FMiddleName,FWords[i]]));
end; // for
end;
procedure TNameExtractor.ParseName;
var
iFirstName: Integer;
i:integer;
iSuffix:Integer;
iLastName:Integer;
iTitle:Integer;
begin
FTitle := '';
FFirstName := '';
FMiddleName := '';
FLastName := '';
FSuffix := '';
ExtractWords(FFullName);
if FWords.Count > 0 then
begin
iSuffix := FindSuffix;
iLastName := findLastName(iSuffix);
iTitle := findTitle(iLastName);
iFirstName := findFirstName(iTitle,iLastName);
findMiddleName(iFirstName,iTitle, iLastName);
end; // if
end;
Business Rule 7
TestComplete:
procedure TestBR7;
begin
try
w.NameExtractor.FullName := 'Major Brown';
Log.Error('No Exception Generated');
except
CheckException('Ambiguous Name',ExceptionMessage,
'Invalid Exception Generated');
end;
end;
Delphi:
procedure TNameExtractor.ParseName;
var
iFirstName: Integer;
i:integer;
iSuffix:Integer;
iLastName:Integer;
iTitle:Integer;
begin
FTitle := '';
FFirstName := '';
FMiddleName := '';
FLastName := '';
FSuffix := '';
ExtractWords(FFullName);
if FWords.Count > 0 then
begin
iSuffix := FindSuffix;
iLastName := findLastName(iSuffix);
iTitle := findTitle(iLastName);
iFirstName := findFirstName(iTitle,iLastName);
findMiddleName(iFirstName,iTitle, iLastName);
if (FFirstName = '') and(LastDelimiter('AEIOUY',UpperCase(FTitle)) <> 0)
then raise ENameExtractorError.Create('Ambiguous Name');
end; // if
end;
Extending the Tests
It would be very tedious to extend the tests to handle a series of names to test. So let us write a test that reads a CSV file of names and expected results. This will take advantage of the built-in routine GetCSVItem of TestComplete Enterprise addition. We will setup the text file in the following format (See 'TestNames.Txt' for example file):
Exception, Title, First Name, Middle Name, Last Name, Suffix, Full Name
The procedure is as follows:
procedure TestReadCSVFile;
var FileTest: OleVariant;
S: string;
Count: Integer;
ExcMsg,
ETitle,
EFirst,
EMiddle,
ELast,
ESuffix,
IFullName:string;
begin
AssignFile(FileTest, Options.Project.Directory + 'TestNames.Txt');
try
Reset(FileTest);
while not EOF(FileTest) do
begin
Readln(FileTest, S);
Count := GetCSVCount(S);
if Count <> 7 then Continue;
ExcMsg := GetCSVItem(S, 0);
ETitle := GetCSVItem(S, 1);
EFirst := GetCSVItem(S, 2);
EMiddle := GetCSVItem(S, 3);
ELast := GetCSVItem(S, 4);
ESuffix := GetCSVItem(S, 5);
IFullName := GetCSVItem(S,6);
Log.Message(Format('Testing Name: %s',[IFullName]),''
,pmHigher, fmBold, clNavy);
if ExcMsg <> '' then
begin
try
w.NameExtractor.FullName := IFullName;
Log.Error('No Exception Generated');
except
CheckException(ExcMsg,ExceptionMessage,
'Invalid Exception Generated');
end;
end
else
begin
w.NameExtractor.FullName := IFullName;
CheckEquals(ETitle,w.NameExtractor.Title,'Title is not correct');
CheckEquals(EFirst,w.NameExtractor.FirstName,
'First Name is not correct');
CheckEquals(EMiddle,w.NameExtractor.MiddleName,
'Middle Name is not correct');
CheckEquals(ELast,w.NameExtractor.LastName,
'Last Name is not correct');
CheckEquals(ESuffix,w.NameExtractor.Suffix,
'Suffix Name is not correct');
end;
end;
finally
CloseFile(FileTest);
end;
end;
The text file needs to be placed in the same directory as the TestComplete project. The project now can easily test hundreds of names by using the CSV file.
Wrapping Up
Unit testing is very good at forcing the developer to work on debugging code while the code is still fresh in his/her mind. This paper shows one way of using TestComplete in testing.
One of the most important things to do with unit testing is:
If a bug is discovered in later testing, to write a test that will uncover the bug in the unit testing. Then fix the bug.
References:
Dave Thomas and Andy Hunt, "Learning to Love Unit Testing", in The Software Testing & Quality Engineering Magazine, January/February 2002, volume 4, issue 1. Pp.32-38.
Martin Fowler, ed., Refactoring: Improving the Design of Existing Code, Addison Wesley Longman, 1999; ISBN 0201485672.
Kent Beck, extreme Programming explained: Embrace Change, Addison Wesley Longman, 2000; ISBN 0201616416
Mark Fewster and Dorthy Graham, Software Test Automation: Effective use of test execution tools, Addison Wesley, 1999, ISBN 0201331403
Edward Kit, Software Testing in the Real World: improving the process, Addison Wesley, 1995, ISBN 0201877562
Elfriede Dustin, Jeff Rashke and John Paul, Automated Software Testing: Introduction, Management and Performance, Addison Wesley, 1999, ISBN 0201432870
Robert V. Binder, Testing Object-Oriented Systems: Models, Patterns and Tools, Addison Wesley, 2000, ISBN 0201809389
Watts S. Humphrey, A Discipline for Software Engineering, Addison Wesley, 1995, ISBN 0201546108
Steve McConnell, Code Complete, Microsoft Press, 1993, ISBN 1556154844
Steve Maguire, Writing Solid Code, Microsoft Press, 1993, ISBN 1556155514
Websites:
http://www.stqemagazine.com
http://www.stickyminds.com
http://www.qaforums.com
http://dunit.sourceforge.net
http://www.xprogramming.com
© 2002 Holton Integration Systems