Tutorial
Tutorial
Abstract
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.
Introduction
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
Refactoring
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.
TObject TTestCase
#SetUp
#TearDown
+Check( test: Bool; msg: str)
TCounter TCounterTest
program CounterObjectTest;
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
GUITestRunner.runRegisteredTests;
end.h
unit CounterTest;
{ 020607 yky Created to illustrate the use of DUnit
This unit will test the TCounter Class.
}
interface
uses
TestFrameWork, CounterCls;
Inheriting TTestCase
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:
type
TCounterTest = class(TTestCase)
protected
mCounter: TCounter;
procedure SetUp; override;
procedure TearDown; override;
published
procedure testDouble;
procedure testPower;
procedure testFactorial;
end;
SetUp
The SetUp function is called for every published test procedure in this Harness.
In this case we need a FObj instantiated every time.
procedure TCounterTest.SetUp;
begin
inherited;
mCounter := TCounter.Create;
end;
TearDown
For everything that has been Created, we will need to destroy. So TearDown
would look something like this:
procedure TCounterTest.TearDown;
begin
inherited;
mCounter.Free;
end;
Now that we have done our housekeeping, we are ready to test the hell out of
mCounter!
TCounter = class(TObject)
published
property input: real;
property DoubleIt: real;
property PowerIt: real;
property FactorialIt: real;
end;
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!
TCounterTest = class(TTestCase)
protected
mCounter: TCounter;
procedure SetUp; override;
procedure TearDown; override;
published
procedure testDouble;
procedure testPower;
procedure testFactorial;
procedure testFactorialNegative;
end;
Using Check
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.
procedure TCounterTest.testDouble;
begin
with mCounter do
begin
input := 5;
Check( input = 5, 'input should be 5');
Check( DoubleIt = 10, 'double of 5 should be 10');
input := 1055;
Check( DoubleIt = 2110, 'double of 1055 should be 2110');
input := 11.23;
Check( abs(DoubleIt - 22.46) < 0.001,
'double should work for fractions ' + FloatToStr( DoubleIt ));
input := -23.43;
Check( abs(DoubleIt - (-46.86)) < 0.001,
'double should work for negatives too');
end;
end;
We need to use the funny abs function < 0.001 because floating point numbers do
not equate well with fractions.
Testing Exceptions
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:
procedure TCounterTest.testFactorialNegative;
begin
with mCounter do
begin
input := -5;
try
Check( FactorialIt > 0, 'Factorial it should raise an exception');
Check( False, 'This should never execute.');
except
on ECounterNegative do Check(True, 'Negative Factorial detected OK');
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.
Registering TTestCase
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.
initialization
RegisterTest('Tutorial/Counter', TCounterTest.Suite);
end.
If there are any Check errors, a pink result will appear. Any uncaught exceptions
will be represented by red colour.
We can then run DUnit again, and expect all the tests to pass.
DUnit in production
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
Overview
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.
Specifications
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:
TTestCase
TObject
#SetUp
#TearDown
+Check( test: Bool; msg: str)
TNumList TNumListTest
+ Nums: Array of real #mNumList
#SetUp
+ Count: integer #TearDown
+testAdd
+ Average: real +testGetNum
+ Total: real +testSetNum
+ Add( pNum: real ) +testTotal
+testAverage
Setting up
the Project
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:
program TNumListTest;
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.
TNumList = class(TObject)
private
FCount: integer;
FNums: array [0..199] of real;
function GetCount: integer;
function GetAverage: real;
function GetTotal: real;
function GetNums(ind: Integer): real;
procedure SetNums(ind: Integer; const Value: real);
public
constructor Create;
procedure Add( pNum: real );
property Nums[ ind: Integer ]: real read GetNums write SetNums;
published
property Count: integer read GetCount;
property Total: real read GetTotal;
property Average: real read GetAverage;
end;
unit NumListTest;
interface
uses
TestFrameWork;
type
TNumListTest = class(TTestCase)
protected
procedure SetUp; override;
procedure TearDown; override;
published
end;
:
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!
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?
Questions
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~