Unit Testing light in Delphi
By A.Bouchez on 2010, Friday July 23, 15:35 - Pascal Programing - Permalink
Automated Unit Testing is a great improvement in coding safe applications.
If you don't know about it, visit http://xprogramming.com/index.php then come back here, and you'll discover how we implement unit testing in a KISS way, in pure Delphi code.
What about automated testing?
You know that testing is (almost) everything if you want to avoid regression problems in your application.
How can you be confident that any change made to your software code won't create any error in other part of the software?
So automated unit testing is the good candidate for implementing this.
And even better, testing-driven coding is great:
0. write a void implementation of a feature, that is code the interface with no
implementation;
1. write a test code;
2. launch the test - it must fail;
3. implement the feature;
4. launch the test - it must pass;
5. add some features, and repeat all previous tests every time you add a new
feature.
It could sounds like a waste of time, but such coding improve your code quality a lot, and, at least, it help you write and optimize every implementation feature.
But don't forget that unit testing is not enough: you have to do tests with your real application, and perform tasks like any user, in order to validate it works as expected. That's why we added the writing and cross-referencing of test protocols in our SynProject documentation tool.
So how is testing implemented in our framework?
We may have used DUnit - http://sourceforge.net/projects/dunit
But I didn't like the fact that it relies on IDE and create separated units for testing. I find it useful to make tests in pure code, in the same unit which implement them. Smartlink of the Delphi compiler won't put the testing code in your final application, so I don't see a lot of drawbacks. And I don't like visual interfaces with red or green lights... I prefer text files and command line. And DUnit code is bigger than mine, and I don't need so many options. That's a matter of taste - you can not agree, that's fine.
So what about using RTTI for adding tests to your program?
The SynCommons unit implements two classes:
type /// a class used to run a suit of test cases TSynTests = class(TSynTest)
which is used to register/list the tests; and
type /// a class implementing a test case // - should handle a test unit, i.e. one or more tests // - individual tests are written in the published methods of this class TSynTestCase = class(TSynTest)
which is the parent of any test case: in published methods of these classes, you write your own tests.
Sample code
Here are the functions we want to test:
function Add(A,B: double): Double; overload; begin result := A+B; end; function Add(A,B: integer): integer; overload; begin result := A+B; end; function Multiply(A,B: double): Double; overload; begin result := A*B; end; function Multiply(A,B: integer): integer; overload; begin result := A*B; end;
So we create three classes one for the whole test suit, one for testing addition, one for testing multiplication:
type
TTestNumbersAdding = class(TSynTestCase)
published
procedure TestIntegerAdd;
procedure TestDoubleAdd;
end;
TTestNumbersMultiplying = class(TSynTestCase)
published
procedure TestIntegerMultiply;
procedure TestDoubleMultiply;
end;
TTestSuit = class(TSynTests)
published
procedure MyTestSuit;
end;
The trick is to create published methods.
Here is how one of these test methods are implemented (I let you guess the others):
procedure TTestNumbersAdding.TestDoubleAdd;
var A,B: double;
i: integer;
begin
for i := 1 to 1000 do
begin
A := Random;
B := Random;
Check(SameValue(A+B,Adding(A,B)));
end;
end;
The SameValue() is necessary because of floating-point precision problem, we can't trust plain = operator.
And here is the test case implementation:
procedure TTestSuit.MyTestSuit; begin AddCase([TTestNumbersAdding,TTestNumbersMultiplying]); end;
And the main program:
with TTestSuit.Create do
try
ToConsole := @Output; // so we will see something on screen
Run;
readln;
finally
Free;
end;
Just run this program, and you'll get:
Suit ------ 1. My test suit 1.1. Numbers adding: - Test integer add: 1000 assertions passed - Test double add: 1000 assertions passed Total failed: 0 / 2000 - Numbers adding PASSED 1.2. Numbers multiplying: - Test integer multiply: 1000 assertions passed - Test double multiply: 1000 assertions passed Total failed: 0 / 2000 - Numbers multiplying PASSED Generated with: Delphi 7 compiler Time elapsed for all tests: 1.96ms Tests performed at 23/07/2010 15:24:30 Total assertions failed for all test suits: 0 / 4000 ! All tests passed successfully.
You can see that all text on screen was created by "uncamelcasing" the method names, and that the test suit just follows the classes defined.
I've uploaded this test in the SQLite3\Sample\07 - SynTest folder of our Source Code Repository.
You can post comments and get feedback in our forum.