020607 yky Modified UML diagram, added some Questionsns 020610 yky Modified changed example to TCounter from TPDObject added UML diag.g.
This document will briefly introduce one aspect of eXtreme Programming (XP) which we will use in our development cycle, the DUnit. It will describe how to Use Test Harnesses to test the code we have written and demonstrate its advantages in refactoring, optimizations, peace of mind and the quality of our software.
DUnit orgininates from an eXtreme Programming module which tests the developers code and confirms that the code or module does what it should.
The concept is simple: write your test harness first and expect your code to fail before implementation. After implementation, ALL tests should pass. If you can think of different scenarios, write the test cases, testing for the correct results. And remember, when a bug is found, the developer will only test that bug once manually because he MUST write a test case to keep testing for that bug in the future.
On unit tests, please refer to this page:
http://www.extremeprogramming.org/rules/unittests.html
DUnit is a member of the *Unit family for all the different programming languages. It is developed as an Open Source project in sourceforge (dunit.sourceforge.net) and can be freely downloaded and used.
It is also important that when we review (clean up ugly code, make things more readable) and optimize (make things faster) we do not break the software. If we have written complete test harnesses, then ALL tests should pass as before. This gives us a lot of opportunity to modify (for the better) existing code but ensuring that the process does not affect the end result.
Refactoring is this process of going back into old code, improving it, and ensuring that the results are the same if not better.
This is especially important for the case of OO Programming where changes in the Base class may have dire consequences to the inherited classes. So if we have tests written throughout the hierarchy, changes which change the behaviour of descendant classes can be detected.
On more about eXtreme Programming as a software development methodology, please go to www.extremeprogramming.org or http://www.xprogramming.com/xpmag/whatisxp.htm
In this Example, we will demonstrate how to
1.create a simple class declaration without its implementation
2.setup the test harness
3.write its test cases
4.run the test harness expecting errors because of the incomplete implementation
5.implement each function one by one
6.continuously test our creation iteratively
The example is to create a simple Counting object called TCounter which takes in an input value and when functions like double, factorial, power are called, it will do the operation on the value and also return the values.
Here is the UML diagram which describes the classes inheritance, the attributes and functions we need to implement.
For example, TCounter will have a TCounterTest, and this should exist in a seperate application from the actual usable app.
Here are the steps to create a Test Harness:
1.Make a New project
2.Remove Unit1 (the default Form unit) from the Project
3.Add to the project these units: GUITestRunner and TestFrameWork which are located in crmpos/Tests/DUnit
4.Replace Application.Initialize and Application.Run with GUITestRunner.runRegisteredTests
5.Thats all you have to do, and the project code should look something like this:
uses CounterCls in 'CounterCls.pas',, CounterTest in 'CounterTest.pas',, TestFramework in '..\..\..\Tests\DUnit\TestFramework.pas', GUITestRunner in '..\..\..\Tests\DUnit\GUITestRunner.pas' {GUITestRunner}; {$R *.res} begin end.h
If we were to test the TCounter class, we will create a new unit and call it CounterTest.pas
In it, we must use TestFrameWork from DUnit. Of course we will also use the objects which we want to test which is CounterCls.pas
{ 020607 yky Created to illustrate the use of DUnitUnit This unit will test the TCounter Class.he TCounter Class. } interface uses , ;
The first thing to do is to create a Test Case Object, and to do so, we inherit from TTestCase.
We will also then override two protected procedures called SetUp and TearDown.
Subsequent Tests will be published procedures which have names starting with 'test'.
The code should look something like this:
TCounterTest = class(TTestCase)) protectedd mCounter: TCounter; procedure SetUp; override;de; procedure TearDown; override;de; publishedd procedure testDouble;le; procedure testPower; procedure testFactorial;al; end;;
The SetUp function is called for every published test procedure in this Harness.
In this case we need a FObj instantiated every time.
begin inherited;; end;
The FObj is usually the object we intend to test.
For everything that has been Created, we will need to destroy. So TearDown would look something like this:
begin inherited;; end;
Now that we have done our housekeeping, we are ready to test the hell out of mCounter!
Before we write the tests, we have to look at the object we are testing and its specifications. We do not need to have the object implemented yet. Here is the object:
publisheded property input: real;eal; property DoubleIt: real;eal; property PowerIt: real;eal; property FactorialIt: real;eal; end;d;
This object is rather basic. Given and input, it can calculate the double with DoubleIt, the power of itself with PowerIt and the factorial with FactorialIt.
Now that we know what our Class to be tested is suppose to do, we can proceed in writing the Test Harness before implementing it!
The first thing to do is write a published procedure called testDouble. The Test Harness Class should look something like this:
protectedd mCounter: TCounter;er; procedure SetUp; override;de; procedure TearDown; override;de; publishedd procedure testPower;er; procedure testFactorial;al; procedure testFactorialNegative;ve; end;;
Check is a DUnit function which takes in two parameters; the first is the logical operation and the second is the error message or information to report if something did go wrong.
This is a test plan for DoubleIt
1.Set the input to 5
2.Check that DoubleIt is 10
3.Check larger numbers of Doubleit
4.Check Fractions of double it
5.Check negative numbers of DoubleIt
begin with mCounter doo beginn input := 5;= 5; Check( input = 5, 'input should be 5');5'); Check( DoubleIt = 10, 'double of 5 should be 10');0'); input := 1055;055; Check( DoubleIt = 2110, 'double of 1055 should be 2110');0'); input := 11.23;.23; Check( abs(DoubleIt - 22.46) < 0.001,001, 'double should work for fractions ' + FloatToStr( DoubleIt ));Str( DoubleIt )); input := -23.43;.43; Check( abs(DoubleIt - (-46.86)) < 0.001,001, 'double should work for negatives too'); negatives too'); end;; end;
We need to use the funny abs function < 0.001 because floating point numbers do not equate well with fractions.
There will definitely be cases where we purposely attempt to break the object by giving it invalid data: Exceptions would be raised during Validations or operations.
In the Counter Class code, we have defined factorial only able to accept positive numbers. Negative numbers will result in an exception being raised.
This is the Test Harness of the plan and code for testFactorialNegative:
1.Set the input value to -5
2.Attempt to get the Factorial Value
3.If an exception was not raised, then report and error in the implementation
4.If an exception called ECounterNegative was caught then the Counter class was constructed well.
Here is the code:
begin with mCounter doo beginn input := -5; -5; try try Check( FactorialIt > 0, 'Factorial it should raise an exception');tion'); Check( False, 'This should never execute.');ute.'); exceptcept on ECounterNegative do Check(True, 'Negative Factorial detected OK'); OK'); end;end; end;; end;
Notice how we use the Check function. In this case, we do not use it to test anything, but hard code True and False values into it to send messages to the DUnit interface.
We always must send a Check(False) if we do not want the execution to continue; where errors that have occurred yet no exceptions were raised.
Use Check(True) just to confirm to ourselves that the execution was correct.
Once we have completed writing the test harness, we will have to register this so that the GUITestRunner can run the tests. To do so, add this in the initialization section of the Unit.
('Tutorial/Counter', TCounterTest.Suite); end.
In RegisterTest, the first parameter describes the Tree or Hierarchy in the placement of the tests.
The second parameter is the TTestCase object which you have just defined. The published test procedures will be displayed in the UI
Just press F9 within Delphi, or run the compiled application directly, and the DUnit interface will appear. Just press the green Play button, and it will run through the tests.
If there are any Check errors, a pink result will appear. Any uncaught exceptions will be represented by red colour.
If everything goes well, then all the tests should be green.
We are now ready to implement the Counter Class. Looks something like this:
begin Result := Finput * 2;; Finput := Result;; end; function TCounter.: real; var i: integer; begin if Finput < 0 thenn .Create('Cannot Factorial a negative number'); Result := 1;; for i := 1 to trunc( Finput ) doo Result := Result * i; i; Finput := Result;; end; function TCounter.: real; begin Result := Finput * Finput;; Finput := Result;; end; procedure TCounter.(const Value: real); begin Finput := Value;; end;
We can then run DUnit again, and expect all the tests to pass.
Eventually the list of tests would be extremely long, covering all aspects of the code we have written. Daily test runs will be run in batch mode and any problems can be easily detected and fixed.
Please use DUnit extensively and update the test cases whenever you detect a new bug, think of a strange scenario or want peace of mind.
DUnit Tutorial
This tutorial will cover the creation of a new object with specific attributes and functions. Before the implementation of this object, a test harness is written to confirm that the object works. Using this harness we can also attempt to break this object, refactor and confirm that the objects works just as before, if not better.
The purpose of this Tutorial is to create an object called TNumList which can do these things:
•collect an array of reals (maximum is 200 numbers)
•Add another real at the end of the list
•keep a Count / Tally of the number of Reals
•Total Up the array of Reals
•Find the Average of the Reals
•Inherites directly from TObject
Here is the UML:
Refer to the “Setting up the Project Test Harness” instructions.
Create two new units and save them as NumListCls and NumListTest.
The resultant code for the project should look something like this:
uses TestFramework in '..\..\..\Tests\DUnit\TestFramework.pas',, GUITestRunner in '..\..\..\Tests\DUnit\GUITestRunner.pas' {GUITestRunner},, NumListCls in 'NumListCls.pas',, NumListTest in 'NumListTest.pas';; {$R *.res} begin GUITestRunner.runRegisteredTests;; end.
We will have to create a skeleton class with the necessary published properties as defined in the UML. So quickly define TNumList as such, without the need to implement. Just make sure it compiles with stub code.
privatee FCount: integer; FNums: array [0..199] of real;al; function GetCount: integer;er; function GetAverage: real;al; function GetTotal: real;al; function GetNums(ind: Integer): real;al; procedure SetNums(ind: Integer; const Value: real);l); publicc constructor Create;te; procedure Add( pNum: real );al ); property Nums[ ind: Integer ]: real read GetNums write SetNums;ms; publishedd property Count: integer read GetCount;nt; property Total: real read GetTotal;Total; property Average: real read GetAverage;age; end;;
!!!! DO NOT PROCEED IN IMPLEMENTING THIS TNumList CLASS !!!!
Now lets set up the Test Harness Unit. Follow the instructions in the section “Making a new Unit Test”
Your code should look like this:
interface uses ; type TNumListTest = class(() protectedd procedure SetUp; override;de; procedure TearDown; override;de; publishedd end;; : :
Now that you have set up your 3 files, you are ready to create the tests prior to implementation. This ensures that you have understood all the requirements and you know what to expect given the pre-conditions and inputs.
Define a protected variable called mNumList of type TNumList. Make sure that your SetUp procedure creates this object and your TearDown frees it.
The first thing we would like to test is the Add procedure.
Here is our test plan for Add:
1.Make sure the NumList has zero Count
2.Add a number like 321 in it
3.Check that the Count has increased by one (no more no less)
4.Add the next number 456
5.Check that the Count has increased again
6.Check using Nums that Nums[0] is equal to 321
7.Check that Nums[1] is 456
That should be enough to test the functionality of Add.
We need to assume that Count, SetNum and GetNum already work for the simple cases. However for more extensive tests, we need testGetNum and testSetNum
Please create a new published procedure in TNumListTest and implement this test plan.
Make sure that mNumList.Nums[2] works as planned. Make a test plan and implement it. It may be quite simple.
Make sure that mNumList.Nums[2] := 343, works as planned. Make a test plan and implement it. It may be quite simple too.
Come up with your own test plan for testTotal, and implement it
Come up with your own test plan for testAverage, and implement it
Using the path of 'Tutorial/NumList', register your TestCase. See the section “Registering TTestCase” on how to do this.
Now that you have created 3 tests you can try running this application by pressing F9. Click on the green Run button, and you should see something like this:
Running this, the DUnit interface reports errors in All of our 3 test. This is as expected because we haven't implemented the TNumList object yet!
Now you can proceed in implementing this simple object. After every step, run the DUnit interface to run through the tests for instant gratification.
1.Implement Add, which increments Count after adding onto the list
2.Implement Total which totals up all the reals up to Count-1
3.Implement Average which is the totals divided by Count
Make a test procedure to test the ability of mNumList.Nums[0] to be altered
We will now proceed with further testing, and this normally called
•What happens when we Access (Get) Nums[-1] or Nums[202] or even Nums[Count]?
•What happens when we write (Set) Nums[-1] or Nums[202] or Nums[Count]?
•What happens when we add more than 200 items in the list? (Please turn on Project/Options/Compiler/Range Checking + Overflow)
•What happens when we try to get the Average without any items in the list?
•What happens when we put very large reals in the list and find the Total? Use MaxDouble from Math
We will now need to decide how to handle these problems.
1.If this object is accessed (either Get or Set) outside its Bounds, i.e. Nums[-1] or Nums[Count] then an exception called ENumListIndexOutOfBounds should be raised
2.If we attempt to add more than 200 items, then an exception called ENumListCapacityExceeded should be raised
3.If we try to get the Average from an empty list, then the result should be zero.
4.If the total is too large for the type an exception called ENumListTotalOverflow should be raised.
Now that you have decided on the peculiar situations, write out the tests to re-create this with the proper exception handling if necessary.
After the test harness has been completed, run DUnit and expect failures.
Go back to the object and build in the necessary new behaviours of the object.
eXtreme Programming is about change. Customers tend to change their minds during the progress of a project. We should not encourage their changes, but we must be prepared that changes do occur.
So the new requirement are:
1.From the previous requirement of only 200 reals to be kept, we now need to keep track of an Unlimited (as much as RAM can handle) number of items
2.There should be a method to delete any real given an index
3.These numbers should be able to be sorted by Values
Given the 3 new requirements, we should consider changing our static Array FNums into a more dynamic TList descendant which can cater for Unlimited entries, sort functions and Deletes. The most user friendly one which comes to mind is a TStringList.
The only drawback is that we will have to convert reals to Strings, which may hinder performance slightly.
But lets go along with that idea anyway, and proceed with this plan in REFACTORING our array into a TStringList.
After we have done so, please run the DUnit checks to make sure that all the test cases work. If it does, congratulations, you have improved your code yet kept the functionality the same. Isnt encapsulation wonderful?
Please proceed in planning and implementing tests for testDelete and testSort
What is the parameter format of Check?
How do I test for an expected Exception?
What will DUnit show if there was an unexpected Exception?
What happens when you dont have any code in a test procedure?
Does the order of the published test procedures matter to the DUnit UI?
Do we delete the trivial tests as we move on to more complex scenarios?
~END~