Increased Productivity with Refactoring, Unit Testing, Help Insight, Error Insight, and Sync Edit in Borland Delphi 2005

By: Bob Swart

Abstract: This tutorial demonstrates the increased productivity gained with Refactoring, Unit Testing, Help Insight, Error Insight and Sync Edit in Borland Delphi 2005.

Building the example application

Help Insight

Error Insight

Refactoring: Declare Variable

Sync Edit

More Refactoring

Refactoring: Extract Method

Refactoring: Rename

Unit Testing

Unit Testing Project

Conclusion

    Introduction

This tutorial will build a line and word count application, reading text files. The example application is built with continuous use of Refactoring, Unit Testing, Help Insight, Error Insight and Sync Editing.

    Building the example application

This tutorial will use the Delphi for .NET personality of Borland Delphi 2005, but you can also use the Delphi Win32 personality (you have to substitute WinForm controls with Win32 VCL controls, but the overall steps and code examples will be the same).

- Start Delphi 2005

- Do File | New and select the "Windows Forms Application - Delphi for .NET" option.

This will create a new WinForms project. You may wish to save the project under a different name in a different directory other than the default name and directory.

- From the Dialogs category in the Tool Palette, double-click on a OpenFileDialog component. This will place a component called OpenFileDialog1 in the non-visual component area of the WinForms Designer.

- From the Windows Forms category in the Tool Palette, double-click on a Button control.

- In the Object Inspector, change the (Name) property of the Button control to "btnOpen". Change the Text property to "Open".

    Help Insight

Help Insight offers additional help on symbols like classes, properties, methods or events. Help Insight can appear in two difference scenarios: either as a stand-alone Help Insight tooltip window, or in combination with a Code Insight window.

- Double-click on the Button in the WinForms Designer, which will bring you to the Code Editor inside the btnOpen_Click event handler.

- Start to write the following lines of code for the Click event (basically type in "OpenFileDialog1." and let the cursor rest after the dot.

procedure TWinForm1.btnOpen_Click(sender: System.Object;
  e: System.EventArgs);
begin
  OpenFileDialog1.
end;

Code Insight will kick in with a pop-up window, showing the available properties and methods of the OpenFileDialog1 component.

- Walk through the Code Insight list of properties and methods, and let the cursor rest at the ShowDialog method.

At this time, Help Insight will also kick in with another pop-up window, next to the Code Insight window, showing information about the selected method, in this case the CommonDialog.ShowDialog method.

Click to see full-sized image

The Help Insight pop-up window often contains links that you can visit for more information or to open the actual source code unit.

- Click on the System.Windows.Forms.DialogResult.OK link in the Help Insight pop-up window.

The Help Insight links will bring you more information (from the local helpfile or the internet), or take you to the source file with the actual declaration of the item.

- Press enter in the Code Insight window with the ShowDialog method selected.

- Complete the source code with the following:

procedure TWinForm1.btnOpen_Click(sender: System.Object;
  e: System.EventArgs);
begin
  if (OpenFileDialog1.ShowDialog =
      System.Windows.Forms.DialogResult.OK) then
  begin
  end;
end;

*Note that we cannot write DialogResult.OK here, because the WinForm itself has a DialogResult property which is closer in scope than the DialogResult type from the System.Windows.Forms namespace, hence the fully qualified expression here.

Apart from additional help in the Code Insight pop-up window, Help Insight also shows up when you move your mouse cursor over identifiers in the Code Editor. For example, if you place the mouse cursor over the OpenFileDialog1 symbol in the event handler you've just written, Help Insight will tell you the place where OpenFileDialog1 is declared (the filename plus position - you can click on that hyperlink to be taken to the declaration), as well as the actual type System.Windows.Forms.OpenFileDialog.

    Error Insight

Error Insight is a useful feature that identifies syntax errors while you're typing. In the previous screenshot, a very little example of Error Insight could be observed since the "end;" statement of the Click event handler was marked with a red wavy underline (at that moment, the "end;" following the "OpenFileDialog1." part).

- Write code to assign a TextFile to the OpenFileDialog1.FileName property, open the file, and close it again, as follows:

procedure TWinForm1.btnOpen_Click(sender: System.Object;
  e: System.EventArgs);
begin
  if (OpenFileDialog1.ShowDialog =
      System.Windows.Forms.DialogResult.OK) then
  begin
    Assign(f,OpenFileDialog1.FileName);
    Reset(f);
    try
    finally
      CloseFile(f);
    end;
  end;
end;

Error Insight will kick in again, and mark all three occurrences of the variable "f" with a red wavy underline.

Click to see full-sized image

*Note that the actual error is shown in a tooltip window when you move the mouse cursor over one of the underlined symbols (in this case the error is "undeclared identifier f"). Apart from the tooltip, the Code Insight errors are also listed under the Errors node in the Structures view in the upper-right corner of the Delphi 2005 IDE. Here, the error as well as corresponding line number is shown. If you click on an error in the Structure view, you are taken to the corresponding location in the Code Editor.

Obviously, this problem is fixed quickly by manually adding a variable f of the correct type. But this is also a good place to demonstrate one of the Refactoring features: declare variable.

    Refactoring: Declare Variable

Refactoring is the process of reshaping existing source code by adding structure to it, generally without changing the behaviour and output of your code (unless you need to declare a variable or field in order to make the code compile), making it easier for actual reuse and maintenance.

- In the Code Editor, place the cursor just before one of the "f" symbols. You can now right-click and select the Refactoring option to view the available choices, or just open the Refactor menu.

There are six different Refactor actions, but not all of them are always available. The choices are Rename, Declare Variable, Declare Field, Extract Method, Extract Resource String or Find Unit.

- Select the Declare Variable choice, which brings up the corresponding dialog where we can specify the type of the new variable f.

The drop-down combobox for Type is filled with a set of predefined types like Byte, AnsiString, WideChar, Double, Pointer, Variant, Real, Char, Boolean, TObject, WideString, string and Integer. The Declare Variable dialog makes a suggestion as to which type is most suited. However, it doesn't know about TextFile type, so we need to enter that here.

- Enter the type "TextFile" in the Type field, and press OK to generate a new variable declaration for f.

The source code should now look as follows (the variable declaration was generated):

procedure TWinForm1.btnOpen_Click(sender: System.Object;
  e: System.EventArgs);
var
  f: TextFile;
begin
  if (OpenFileDialog1.ShowDialog =
      System.Windows.Forms.DialogResult.OK) then
  begin
    Assign(f,OpenFileDialog1.FileName);
    Reset(f);
    try
    finally
      CloseFile(f);
    end;
  end;
end;

*Note that although a new variable declaration has been added to the start of the Click event method, the position of the cursor in the Code Editor has not changed. This is especially beneficial if you edit longer methods and don't want to have to find your original position to continue to work (instead you just use the Declare Variable dialog which will add the variable declaration for you).

Instead of declaring a variable to the method, you could also have chosen to declare a field inside the class itself using the Declare Field option.

    Sync Edit

Sync Edit allows you to change duplicate identifiers in a section of selected code (allowing you to rename them all at once, for example).

- In the Code Editor, select the code inside the btnOpen_Click event handler. Make sure to include the variable declaration for f as well.

You will now see a special icon in the left side of the code editor, overlapping the line numbers. This icon can be used to switch to Sync Edit Mode. The keyboard shortcut is Shift+Ctrl+J.

- Click on the Sync Edit Mode icon or hit Shift+Ctrl+J to get in Sync Edit Mode.

Once in Sync Edit Mode, the background color of the selected text changes, and the cursor is now used as selector of identifiers that are used in multiple places within the selected text. Any duplicated identifiers will already be marked; in the example source code those will be occurrences of f as well as OpenFileDialog1.

You can use the Tab key to navigate between the duplicate identifiers (f and OpenFileDialog1).

- Press the Tab key to move to the OpenFileDialog1 duplicate identifier, and press Tab to get back to the f duplicate identifier (just to give you a feeling how to navigate).

- With the f duplicate identifier selected, type a new value for f, for example "InputFile".

While you type a new name for f, all occurrences of f will be renamed at the same time, hence the name Sync Edit.

- When you're done, either click on the Sync Edit Mode button again, or click anywhere in the Code Editor with the mouse to move back to normal edit mode.

The source code should now look as follows:

procedure TWinForm1.btnOpen_Click(sender: System.Object;
  e: System.EventArgs);
var
  InputFile: TextFile;
begin
  if (OpenFileDialog1.ShowDialog =
      System.Windows.Forms.DialogResult.OK) then
  begin
    Assign(InputFile,OpenFileDialog1.FileName);
    Reset(InputFile);
    try
    finally
      CloseFile(InputFile);
    end;
  end;
end;

*Note that the Sync Edit feature doesn't actually parse the identifiers, and is therefore best used for small portions of source code (like a routine or method implementation). For renaming identifiers within larger portions of source code, it's recommended to use the Refactoring features instead.

    More Refactoring

Before we can apply any of the other Refactoring techniques, let's first write some more code to actually count the number of lines, as well as the number of words in each line.

- Extend the code within the try-finally block by adding source code to count lines and words, as follows:

procedure TWinForm1.btnOpen_Click(sender: System.Object;
  e: System.EventArgs);
var
  InputFile: TextFile;
  Line: String;
  LineCount: Integer;
  WordCount: Integer;
  i: Integer;
  InWord: Boolean;

begin
  if (OpenFileDialog1.ShowDialog =
      System.Windows.Forms.DialogResult.OK) then
  begin
    Assign(InputFile,OpenFileDialog1.FileName);
    Reset(InputFile);
    try
      WordCount := 0;
      LineCount := 0;
      while not eof(InputFile) do
      begin
        readln(InputFile, Line);
        Inc(LineCount);
        InWord := False;
        for i:=1 to Length(Line) do
        begin
          if Line[i] in ['a'..'z','A'..'Z','0'..'9','_'] then
          begin
            if not InWord then Inc(WordCount);
            InWord := True;
          end
          else InWord := False;
        end;
      end;
      MessageBox.Show(LineCount.ToString + ' lines with ' +
        WordCount.ToString + ' words in file ' +
        OpenFileDialog1.FileName);
    finally
      CloseFile(InputFile);
    end;
  end;
end;

*Note that this code will treat words as a sequence of letters, digits and/or the _ character. You may want to modify this code to include the . as well, so namespaces are recognised as whole words (and you may remove the _ from the set of valid characters). That's left as exercise for the reader.

    Refactoring: Extract Method

The code to count words may be something you want to put in its own routine. This means it's probably easier to reuse, test and maintain. You can use the Refactoring Extract Method to do this.

- Select the code that is used to count the words. You should start the selection with the line that sets InWord to False, stop the selection with the "end" of the for-loop.

The following code should be selected:

        InWord := False;
        for i:=1 to Length(Line) do
        begin
          if Line[i] in ['a'..'z','A'..'Z','0'..'9','_'] then
          begin
            if not InWord then Inc(WordCount);
            InWord := True;
          end
          else InWord := False;
        end;

- Open the Refactor menu, and select the option "Extract Method".

This will present you with the Extract Method dialog, where you can enter a new name for the method, and get a preview of the method that will be produced.

- Enter "CountNumberOfWordsInString" as new method name.

Click to see full-sized image

The new method has two arguments: WordCount and Line, and two local variables: InWord and i. WordCount will be modified by the new method, so it's a var parameter, but Line is only used as input, so it's a value parameter.

The original code in the btnOpen no longer contains the InWord and i variables (these have been moved to the new CountNumberOfWordsInString method), and the extracted code has been replaced by a call to CountNumberOfWordsInString, passing WordCount and Line as arguments:

procedure TWinForm1.btnOpen_Click(sender: System.Object;
  e: System.EventArgs);
var
  InputFile: TextFile;
  Line: String;
  LineCount: Integer;
  WordCount: Integer;
begin
  if (OpenFileDialog1.ShowDialog =
      System.Windows.Forms.DialogResult.OK) then
  begin
    Assign(InputFile,OpenFileDialog1.FileName);
    Reset(InputFile);
    try
      WordCount := 0;
      LineCount := 0;
      while not eof(InputFile) do
      begin
        readln(InputFile, Line);
        Inc(LineCount);
        CountNumberOfWordsInString(WordCount, Line);
      end;
      MessageBox.Show(LineCount.ToString + ' lines with ' +
        WordCount.ToString + ' words in file ' +
        OpenFileDialog1.FileName);
    finally
      CloseFile(InputFile);
    end;
  end;
end;

- Move the mouse cursor over the call to the CountNumberOfWordsInString method. Help Insight will now show the definition location.

- Click on the link to go to the CountNumberOfWordsInString definition.

- Change the visibility specifier from "private" to "public", so we can use the method CountNumberOfWordsInString in other places as well (like Unit Testing).

    Refactoring: Rename

You may have noticed that all this time the Windows Form class you've been working with is still called TWinForm1. You may want to rename this into something more meaningful, but this actually means editing more than one file (since the TWinForm1 class is also used in your main project file, where the TWinForm1.Create statement is executed).

Obviously, you need something a bit more powerful and overseeing than Sync Edit. The Refactor menu has the Rename option that's suited for this task.

- In the Code Editor, place the cursor in or just before one of the TWinForm1 symbols.

- Open the Refactor menu, and select the Rename option.

This will present you with the Rename dialog

*Note the option to "View references before refactoring", which allows you to see and verify all changes that will be made, before the actual Refactoring takes place.

- Click on OK to view the references before Refactoring.

This produces the following overview:

According to the overview, Refactoring will make changes to the main project file, the WinForm1.pas file, and also needs to update the designer. If you're happy with these proposed changes, click on the Refactor button in the upper-left corner of the Refactoring window to apply the Refactorings.

- Click on the Refactor button in the upper-left corner of the Refactoring window to apply the Refactorings.

- Recompile the project to see if all Refactorings have been applied successfully.

It's now time to add some Unit Testing capabilities to the project, before we make some additional changes.

    Unit Testing

Unit Testing is a methodology of adding tests to your code in such a way, that the tests themselves can be run and verified by a test project, reporting the continued validity of your source code.

For best results, unit testing should be applied right from the start, adding tests to your classes as you write the actual code itself. And since you've just extracted the CountNumberOfWordsInString method in order to make it more reusable, testable and maintainable, let's add some unit tests to this method.

    Unit Testing Project

Unit Testing is not added to the project itself (you don't want to deploy unit tests) but rather is performed in a separate project that is best maintained next to the original project in a single project group.

- Do File | Save All to save your existing work.

- Right-click on the Project Group node in the Project Manager and select "Add New Project".

- In the Object Repository dialog that follows, go to the Unit Test category, and select the Test Project item.

- Click on OK to get the Test Project Wizard.

On the first page of the Test Project Wizard, you can specify the project name (which by default is based on the project that we want to test), as well as the location. The location will by default be the Test subdirectory of the current directory.

- Click on Next to go to the next page in the Test Project Wizard.

Here we can select the Test Framework (DUnit / Delphi.NET or NUnit Delphi.NET) and can select the GUI or Console runner for our unit tests.

By default, the choice is set to DUnit / Delphi.NET and the GUI Unit Test runner.

- Make sure NUnit / Delphi.NET is selected as Test Framework.

-  Click on Finish to add the new Test Project to the project group in the Project Manager.

*Note that a Test Project is still empty. You should now add actual tests to it. This is simple, since the new Test Project is already the active project in the project manager.

-  Do File | New, select Other, and in the Unit Test category of the Object Repository this time select the Test Case icon.

-  Click on OK in order to get the Test Case Wizard.

The Test Case Wizard can be used to select the source file you want to test, and from the selected source file the available classes and method.

- In the Test Case Wizard, click on the ellipsis next to the source file combobox, and in the Open File dialog select WinForm1.pas as source file.

This will produce a list of classes and methods from the WinForm1.pas unit. By default everything is checked, but you can clear methods for which you do not want to write any unit tests. If no methods are checked at all, then a skeleton test case will be generated, where you can add you own unit tests later.

- Click on Nest to move to the second page of the Test Case Wizard.

Here you can specify the project to add the test to, the filename to use for this new test, the test framework (which is determined based on the selected test project), and finally the test base class, which is TTestCase for DUnit, and can be left empty or set to TestCase for NUnit.

Note that Delphi will automatically provide all values for you, but you can override them if you wish (for example to specify a custom filename for the test itself).

- For the NUnit / Delphi.NET Test Case, make sure the Base Class is set to TestCase.

- Click on Finish to accept the default values, generate the Unit Test Case, and add it to the Unit Test Project.

The project group now contains two projects: one for the tutorial, and one for the Unit Test project. Note that the Unit Test Project was added in a Test subdirectory, so the project contains a .. folder to reference the WinForm1.pas source file.

However, we've added a WinForm to it, as host of the method to test, so we should also make sure the NUnit Test Project knows about System.Windows.Forms. Specifically, if you try to compile the Unit Test project, you will get a compiler error in the uses clause of the TestWinForm1.pas file, and the place where the WinForm1.pas unit is included. At that point, the compiler complains that it cannot find file System.Windows.Forms.dcuil (which is one of the namespaces in the uses clause of the WinForm1.pas unit).

This is due to the fact that the method we want to test is part of a Windows Form.

- Right-click on the NUnit Test project, and select "Add Reference".

This will show the Add Reference dialog, where you can specify which assemblies to add as reference to the project.

- Select the System.Windows.Forms assembly, double-click on it to move it to the list of New References.

- Click on OK to actually add the reference to System.Windows.Forms to the NUnit Test Project.

Now the NUnit Test Project will compile without further complaints.

You can now edit the TestWinForm1.pas unit to add the actual Unit Test code. The Test Unit contains a class definition for TestTWordCountWinForm which contains a test method TestCountNumberOfWordsInString that contains the following generated code:

procedure TestTWordCountWinForm.TestCountNumberOfWordsInString;
var
  Line: string;
  WordCount: Integer;
begin
  FordCountWinForm.CountNumberOfWordsInString(WordCount, Line);
  // TODO: Validate method results
end;

As the comment indicates, you should write code to validate the method results (which also means you must make sure the input parameters have received corresponding test values).

*NoteNUnit and DUnit both contains a number of asserts or functions that you can use in the testing code. Unfortunately, these functions are different in the two Unit Testing Frameworks. In this tutorial, I've decided to use NUnit, so we'll use the NUnit Asserts. See the on-line help for more information about DUnit and NUnit and the test functions they offer for us to use in our testing code.

You can use the following NUnit Assert method: AssertEquals, AssertNotNull, AssertNull, AssertSame, and Fail. Alternately, you can use the NUnit Assert object itself, and call methods from this object. However, since Delphi 2005 already defines Assert, you should explicitly qualify the NUnit Assert object as NUnit.Framework.Assert.

- Write code to initialize the Line and WordCount fields, and check the result afterwards, using NUnit as follows:

procedure TestTWordCountWinForm.TestCountNumberOfWordsInString;
var
  Line: string;
  WordCount: Integer;
begin
  WordCount := 0;
  Line := 'This test contains five words';
  FordCountWinForm.CountNumberOfWordsInString(WordCount, Line);
  // Validate method results
  AssertEquals(5, WordCount); // Expected: 5
end;

- Save your work, and compile the NUnit Test Project.

*Note that NUnit produces an assembly, which must be loaded in the NUnit Test Runner, which should be installed separately (an icon to start NUnit-GUI 2.2 should be on your desktop or in the Start menu).

- Inside NUnit, do File | Open, and select the assembly of the NUnit Test Project that you've just produced.

This will load the assembly, and list the Classes, Test Classes, and Test Methods.

- Click on the Run button to run all tests (well, there's only one at this time).

The output will be green lights if the test was successful, or red light if an assert failed.

Obviously, in this case the test was successful.

Click to see full-sized image

You can now add more tests for the same CountNumberOfWordsInString routine. The best way is not to place them in the same test method (since that would not tell you which test has failed), but place them in separate test routines.

- Go to the class definition for TestTWordCountWinForm, and add another test method, as follows:

type
  // Test methods for class TWordCountWinForm
  [TestFixture]
  TestTWordCountWinForm = class(TestCase)
  strict private
    FordCountWinForm: TWordCountWinForm;
  public
    [SetUp]
    procedure SetUp;
    [TearDown]
    procedure TearDown;
  published
    [Test]
    procedure TestCountNumberOfWordsInString;
    [Test]
    procedure TestCountNumberOfWordsInStringAgain;
  end;

- Place the mouse cursor inside the class definition, and hit Ctrl+Shift+C to generate the skeleton implementation for procedure TestCountNumberOfWordsInStringAgain.

- Implement procedure TestCountNumberOfWordsInStringAgain as follows:

procedure TestTWordCountWinForm.TestCountNumberOfWordsInString;
var
  Line: string;
  WordCount: Integer;
begin
  WordCount := 0;
  Line := 'This test (i.e. verification) contains seven words';
  FordCountWinForm.CountNumberOfWordsInString(WordCount, Line);
  // Validate method results
  NUnit.Framework.Assert.AreEqual(7, WordCount);
end;

You may have realised what's going to happen. The sentence "This test (i.e. example) contains seven words." may contain seven words according to itself, the word "i.e." is probably not recognized as one word by the CountNumberOfWordsInString routine.

- Run the Unit Test in the NUnit Test Runner. This time, there will be a failure.

Click to see full-sized image

The expected result of TestCountNumberOfWordsInStringAgain was 7, but the actual result was 8. We should go back into the original project and fix that.

- Open the WinForm1.pas file in the Code Editor, and change the implementation of the CountNumberOfWordsInString to include a . as part of a word.

procedure TWordCountWinForm.CountNumberOfWordsInString(var WordCount: Integer;
  Line: string);
var
  InWord: Boolean;
  i: Integer;
begin
  InWord := False;
  for i := 1 to Length(Line) do
  begin
    if Line[i] in ['a'..'z', 'A'..'Z', '0'..'9', '_', '.'] then
    begin
      if not InWord then
        Inc(WordCount);
      InWord := True;
    end
    else
      InWord := False;
  end;
end;

This will ensure that "i.e." is considered as one word.

- Return to the NUnit Test Project, and recompile the project (make sure the correct project has been activated in the Project Manager).

-  Return to the NUnit-GUI 2.2 Test Runner. Click on the Run button again, this time the test has succeeded.

Click to see full-sized image

Unit Testing helps to increase the quality, maintainability and reuse capabilities of your code, and having unit testing integrated into the Delphi 2005 IDE makes it even easier to implement.

    Conclusion

This tutorial built a line and word count application, reading text files. The example application was built with continuous use and demonstrations of the Refactoring, Unit Testing, Help Insight, Error Insight and Sync Editing features.


Server Response from: ETNASC03