AutomatedQA: Award-winning tools for software development and quality assurance

Home » Technical Papers » Technical Papers - Unit Testing With TestComplete

Unit Testing With TestComplete

Eric Holton

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

  1. The name will be broken apart in to a word list.
  2. Titles will come from a preset list.
  3. Suffix:
    1. Starting from the end of the word list, all words that end in a period are suffixes.
    2. Starting from the end of the word list, all words after the final comma are suffixes
    3. 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.
  4. Last Name:
    1. Last Name is the word before the comma in word list, if any.
    2. If there is only one word it is the last name
    3. 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.
    4. If there are more than two words, the last word in the list, which is not a suffix, is the Last Name.
  5. 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.
  6. 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.
  7. 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:

Description Initial Routine Main Routine Final Routine
Unit Test for Name Extractor unMain.SetupTests   unMain.FinalizeTests

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

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

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

Copyright © 1999- 2008, AutomatedQA, Corp. All Rights Reserved.
Home | Legal | About | Contact | Site Map | Print