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.
Contents
- 1 What is kbmUnitTest?
- 2 Installing the Framework
- 3 Creating a Test Project with the IDE Wizard
- 4 Creating a Test Project by Hand
- 5 Writing Your First Test Fixture
- 6 Running the Tests
- 7 The Assert Class — Quick Reference
- 8 Adding a Description to Your Tests
- 9 What Happens When a Test Fails?
- 10 Entry Points — RunTests, RunTestsAndHalt, RunTestsForCI
- 11 Summary
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:
| Unit | Purpose |
|---|---|
kbmTestFramework.pas | Attributes, Assert class, fluent API, constraints |
kbmTestRunner.pas | Test 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:
- Open the Delphi IDE.
- Choose File → New → Other.
- In the New Items dialog, select the kbmUnitTest category on the left.
- Pick kbmUnitTest – Test Project and click OK.
- 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;interfaceuses 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-insensitiveAssert.AreEqual(42, MyInt);Assert.AreEqual(3.14, MyFloat, 0.01); // tolerance of 0.01Assert.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 PointerAssert.IsNotNull(MyObject);
String Checks
Assert.IsEmpty(S);Assert.IsNotEmpty(S);Assert.Contains('ell', 'hello'); // substringAssert.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 classAssert.WillRaise( procedure begin SomeCode end, EArgumentException);// Verify specific exception class and message contentAssert.WillRaiseWithMessage( procedure begin SomeCode end, EArgumentException, 'out of range');// Verify that NO exception is raisedAssert.WillNotRaise(procedure begin SomeCode end);
Control Flow
Assert.Pass; // explicitly mark a test as passedAssert.Fail('Not implemented'); // explicitly failAssert.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 = classpublic [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, andRunTestsForCI.
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.
![]()







