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.