The Complete Guide to kbmUnitTest

This is part 1 of a 6-part series covering the kbmUnitTest framework for Delphi, from first steps to advanced mocking and CI/CD integration.


What is kbmUnitTest?

kbmUnitTest is a lightweight, self-contained unit testing framework for Delphi. It uses an attribute-based, DUnitX-style API with zero external dependencies — everything you need ships in a handful of Pascal units. If you have used DUnitX, NUnit, or JUnit, the conventions will feel instantly familiar: you mark test fixtures with [TestFixture], individual tests with [Test], and call static methods on an Assert class to verify behavior.

The framework is authored by Kim Bo Madsen at Components4Developers and covers far more than basic assertions: fluent API chains, constraint objects, parameterized tests, memory leak detection, JUnit XML output for CI servers, TestInsight integration, TDataSet assertions, and a full mock data system with code generation wizards. We will explore all of these across this series.

In this first installment we will set up a test project, write a few simple tests, and run them.

Installing the Framework

kbmUnitTest ships as source files. Copy the framework units into a location that your project can reach — for example a Lib\kbmUnitTest subfolder or a shared library path registered in the IDE’s Library Path.

The core files you need for testing are:

UnitPurpose
kbmTestFramework.pasAttributes, Assert class, fluent API, constraints
kbmTestRunner.pasTest discovery, execution engine, console & JUnit loggers

That is it. Two units, no packages to install, no runtime DLLs. Additional optional units exist for memory-leak analyzers, TestInsight integration, TDataSet assertions, and mock data — we will add them in later parts of this series.

Creating a Test Project with the IDE Wizard

If you have installed the kbmUnitTest IDE package (kbmTestWizardPackage.bpl), the fastest way to start is through the wizard:

  1. Open the Delphi IDE.
  2. Choose File → New → Other.
  3. In the New Items dialog, select the kbmUnitTest category on the left.
  4. Pick kbmUnitTest – Test Project and click OK.
  5. The wizard creates a console application pre-configured with the correct {$APPTYPE CONSOLE} and {$STRONGLINKTYPES ON} directives, adds the framework units, and generates a sample test fixture to get you started.

Alternatively you can use the Tools → kbmUnitTest New Test Project menu entry.

If you prefer to do things manually, creating a test project by hand takes only a minute.

Creating a Test Project by Hand

Create a new Console Application in the IDE, then replace the default DPR content with:

program MyTests;
{$APPTYPE CONSOLE}
// STRONGLINKTYPES ensures RTTI is emitted for all types, even those
// that are only referenced indirectly. Without this the test runner
// may fail to discover some fixtures.
{$STRONGLINKTYPES ON}
uses
kbmTestFramework,
kbmTestRunner,
Test.Calculator in 'Test.Calculator.pas';
begin
// RunTests runs all discovered test fixtures, prints color-coded
// console output, and returns True if every test passed.
RunTests;
end.

The {$STRONGLINKTYPES ON} directive is important. The test runner discovers fixtures through RTTI, and the linker will strip RTTI for types it considers unused unless you tell it otherwise.

Writing Your First Test Fixture

Create a new unit called Test.Calculator.pas. We will test a trivially simple calculator record:

unit Test.Calculator;
interface
uses
kbmTestFramework;
type
// The system under test — a simple calculator.
TCalculator = record
class function Add(A, B: Integer): Integer; static;
class function Divide(A, B: Integer): Double; static;
end;
// The test fixture.
[TestFixture]
TTestCalculator = class
public
[Test]
procedure TestAddPositive;
[Test]
procedure TestAddNegative;
[Test]
procedure TestDivide;
[Test]
procedure TestDivideByZero;
end;
implementation
{ TCalculator }
class function TCalculator.Add(A, B: Integer): Integer;
begin
Result := A + B;
end;
class function TCalculator.Divide(A, B: Integer): Double;
begin
if B = 0 then
raise EDivByZero.Create('Division by zero');
Result := A / B;
end;
{ TTestCalculator }
procedure TTestCalculator.TestAddPositive;
begin
Assert.AreEqual(4, TCalculator.Add(2, 2));
end;
procedure TTestCalculator.TestAddNegative;
begin
Assert.AreEqual(-3, TCalculator.Add(-1, -2));
end;
procedure TTestCalculator.TestDivide;
begin
Assert.AreEqual(2.5, TCalculator.Divide(5, 2));
end;
procedure TTestCalculator.TestDivideByZero;
begin
Assert.WillRaise(
procedure begin TCalculator.Divide(1, 0) end,
EDivByZero);
end;
end.

Let us walk through what is happening:

[TestFixture] marks TTestCalculator as a class whose public methods should be scanned for tests. You can optionally give it a display name: [TestFixture('Calculator Tests')].

[Test] marks each method as a test. The runner creates a fresh instance of the fixture class for each test, calls the method, and records whether it passed, failed, or raised an unexpected exception. You can add a description: [Test('Adding two positive numbers')].

Assert.AreEqual compares an expected value to an actual value. Overloads exist for string, Integer, Int64, Double (with optional tolerance), Boolean, and TGUID.

Assert.WillRaise verifies that a given anonymous procedure raises a specific exception class. If the exception is not raised, or a different exception class is raised, the test fails.

Running the Tests

Press F9 (or Run). You will see color-coded console output:

kbm Test Runner
========================================
[Fixture] TTestCalculator
TestAddPositive (0.01 ms)
TestAddNegative (0.00 ms)
TestDivide (0.00 ms)
TestDivideByZero (0.01 ms)
========================================
4 passed, 0 failed, 0 errors, 0 skipped
========================================

Passing tests are shown in green, failures in red, errors (unexpected exceptions) also in red, and skipped tests in yellow. When running under the IDE debugger, results are also written to the Event Log (View → Debug Windows → Event Log) so you do not even need to switch to the console.

The Assert Class — Quick Reference

The Assert class is the workhorse of the framework. Here are the most commonly used methods:

Equality

Assert.AreEqual('hello', MyString);
Assert.AreEqual('HELLO', MyString, True); // case-insensitive
Assert.AreEqual(42, MyInt);
Assert.AreEqual(3.14, MyFloat, 0.01); // tolerance of 0.01
Assert.AreEqual(True, MyBool);
Assert.AreNotEqual('world', MyString);
Assert.AreNotEqual(0, MyInt);

Boolean

Assert.IsTrue(X > 0);
Assert.IsFalse(List.IsEmpty);

Null / Assigned

Assert.IsNull(MyObject); // TObject, IInterface, or Pointer
Assert.IsNotNull(MyObject);

String Checks

Assert.IsEmpty(S);
Assert.IsNotEmpty(S);
Assert.Contains('ell', 'hello'); // substring
Assert.StartsWith('hel', 'hello');
Assert.EndsWith('llo', 'hello');

Numeric Comparisons

Assert.IsGreaterThan(Score, 90.0);
Assert.IsLessThan(Elapsed, 1000.0);
Assert.InRange(Percentage, 0.0, 100.0);

Type Checks

Assert.InheritsFrom(MyObj, TComponent);
Assert.Implements(MyObj, IMyInterface);

Exception Checks

// Verify specific exception class
Assert.WillRaise(
procedure begin SomeCode end,
EArgumentException);
// Verify specific exception class and message content
Assert.WillRaiseWithMessage(
procedure begin SomeCode end,
EArgumentException,
'out of range');
// Verify that NO exception is raised
Assert.WillNotRaise(procedure begin SomeCode end);

Control Flow

Assert.Pass; // explicitly mark a test as passed
Assert.Fail('Not implemented'); // explicitly fail
Assert.Skip('Needs database'); // skip / ignore at runtime

Every assert method accepts an optional trailing AMessage parameter that is included in the failure output:

Assert.AreEqual(42, X, 'X should have been the answer');

Adding a Description to Your Tests

Both [TestFixture] and [Test] accept an optional description string. This description appears in the console output and in JUnit XML reports instead of the bare method name:

[TestFixture('Calculator - Basic arithmetic')]
TTestCalculator = class
public
[Test('Adding two positive integers returns their sum')]
procedure TestAddPositive;
end;

Use descriptions when the method name alone is not self-documenting. For simple, well-named methods like TestAddPositive, the attribute without arguments is perfectly fine.

What Happens When a Test Fails?

When an assertion fails, the framework raises ETestFailure at the calling site using Delphi’s ReturnAddress mechanism. This means that if you are running under the debugger, the IDE breaks at the exact line of your test where the assertion failed — not somewhere deep inside the framework. This is a huge help during debugging.

The failure message includes both the expected and actual values:

FAIL TestAddPositive
Expected <5> but was <4>

Entry Points — RunTests, RunTestsAndHalt, RunTestsForCI

The framework provides three convenience functions in kbmTestRunner.pas:

RunTests — Runs all discovered tests, prints results, and returns True if all passed. Does not terminate the application, so the console stays open when running from the IDE.

if RunTests then
WriteLn('All good!')
else
WriteLn('Some tests failed.');

RunTestsAndHalt — Runs tests and sets ExitCode to 0 (all passed), 1 (failures), or 2 (fatal exception). Designed for use in build scripts and CI where you want the process to exit immediately.

begin
RunTestsAndHalt;
end.

RunTestsForCI — Same as above but also writes a JUnit XML file that Jenkins, GitHub Actions, Azure DevOps, GitLab CI, and virtually all CI systems can consume.

begin
RunTestsForCI('test-results.xml');
end.

All three accept optional parameters for category filtering, verbosity, and memory leak detection:

RunTests(
'Integration', // only run tests in the 'Integration' category
True, // verbose output
True, // enable memory leak detection
lrmFailure); // leaks cause test failure

Summary

In this first part we have covered:

  • Installing the framework (two source files, no packages).
  • Creating a test project using the IDE wizard or by hand.
  • Writing a test fixture with [TestFixture] and [Test].
  • Using Assert.AreEqual, Assert.WillRaise, and other common assertions.
  • Running tests with color-coded console output.
  • Choosing between RunTests, RunTestsAndHalt, and RunTestsForCI.

In Part 2 we will add TestInsight integration so tests run automatically every time you save, with green and red indicators right in the IDE gutter.

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.