The Complete Guide to kbmUnitTest
This is part 4 of a 6-part series. Parts 1–3 covered the test framework itself.
Contents
- 1 The Problem Mock Data Solves
- 2 Defining a Scenario
- 3 The Registry Lifecycle
- 4 Retrieving a Scenario
- 5 Materializing Scenarios into Records
- 6 Materializing into Objects
- 7 Loading Scenarios from JSON
- 8 Loading from CSV
- 9 Capturing from Records and Objects
- 10 RTTI Attribute Mapping
- 11 Exporting Scenarios
- 12 Summary
The Problem Mock Data Solves
Every non-trivial test needs data. An order processing test needs an order. A report test needs a customer list. A validation test needs both valid and invalid inputs. When that data is scattered across test methods — literal strings, magic numbers, anonymous arrays — your test suite becomes hard to read and harder to maintain.
kbmUnitTest’s mock data system solves this with named scenarios: centrally defined datasets that any test can retrieve by name and materialize into Delphi records, objects, TDataSet instances, or raw field-by-field access. Scenarios support inheritance (a “VIP customer” scenario inherits from “customer” and overrides a few fields), tabular data (multiple rows), generators for random/sequential values, and loading from JSON or CSV files.
The mock data system lives in a few optional units:
| Unit | Purpose |
|---|---|
kbmUnitTest.Mock | Core: registry, scenarios, builder, materialization |
kbmUnitTest.Mock.Generators | Value generators: Gen.IntRange, Gen.Sequential, etc. |
kbmUnitTest.Mock.DataSet | Bridge to materialize scenarios as TClientDataSet, TkbmMemTable, or any other TDataSet descendant |
Add what you need to your uses clause. The core unit has no dependency on Data.DB, so if you are not testing datasets, you do not pull in database units.
Defining a Scenario
Scenarios are registered on the global TkbmMockRegistry using a fluent builder. You typically do this in the initialization section of a unit, before any tests run:
unit Test.MockData;interfaceimplementationuses kbmUnitTest.Mock;initialization TkbmMockRegistry.Scenario('customer_alice') .Field('Name', 'Alice Johnson') .Field('Email', 'alice@example.com') .Field('Age', 34) .Field('Active', True) .Field('Balance', 1250.75); TkbmMockRegistry.Scenario('customer_bob') .Field('Name', 'Bob Smith') .Field('Email', 'bob@example.com') .Field('Age', 28) .Field('Active', False) .Field('Balance', 0.0);end.
Each .Field(name, value) call adds a named value to the scenario. Values can be any type that fits in a TValue — strings, integers, floats, booleans, TDateTime, enums, and more.
The fluent builder returns a TkbmMockScenarioBuilder record, so the chain is stack-allocated and requires no memory management.
The Registry Lifecycle
The registry has three phases:
- Open — During
initializationsections, scenarios are registered.Scenario()calls work. - Sealed — Once
TkbmMockRegistry.Sealis called, registration is closed andGet()/TryGet()calls work. The test runner callsSealautomatically, but you can call it explicitly in your initialization if tests and scenario registration are in the same unit. - Complete — After all tests finish,
Completeis called to allow cleanup.
In practice, you register scenarios in initialization sections, and the framework handles the rest.
Retrieving a Scenario
In your test, call TkbmMockRegistry.Get('name') to retrieve a scenario, then read individual fields:
uses kbmTestFramework, kbmUnitTest.Mock;[TestFixture]TTestCustomerValidation = classpublic [Test] procedure TestActiveCustomerHasEmail; [Test] procedure TestInactiveCustomerCanHaveZeroBalance;end;procedure TTestCustomerValidation.TestActiveCustomerHasEmail;var LScenario: TkbmMockScenario;begin LScenario := TkbmMockRegistry.Get('customer_alice'); Assert.IsTrue(LScenario.GetFieldValue('Active').AsBoolean); Assert.That(LScenario.GetFieldValue('Email').AsString) .IsNotEmpty .Contains('@');end;procedure TTestCustomerValidation.TestInactiveCustomerCanHaveZeroBalance;var LScenario: TkbmMockScenario;begin LScenario := TkbmMockRegistry.Get('customer_bob'); Assert.IsFalse(LScenario.GetFieldValue('Active').AsBoolean); Assert.AreEqual(0.0, LScenario.GetFieldValue('Balance').AsExtended);end;
TryGet is available if the scenario might not exist:
var LScenario: TkbmMockScenario;begin if TkbmMockRegistry.TryGet('optional_data', LScenario) then // use it else Assert.Skip('Optional test data not registered');end;
You can also check existence with TkbmMockRegistry.Exists('name').
Materializing Scenarios into Records
Reading fields individually is fine for a few values, but when you have a Delphi record that matches the scenario shape, AsRecord<T> maps fields automatically by name:
type TCustomerRec = record Name: string; Email: string; Age: Integer; Active: Boolean; Balance: Double; end;procedure TTestCustomer.TestAliceRecord;var LCustomer: TCustomerRec;begin LCustomer := TkbmMockRegistry.Get('customer_alice').AsRecord<TCustomerRec>; Assert.AreEqual('Alice Johnson', LCustomer.Name); Assert.AreEqual(34, LCustomer.Age); Assert.IsTrue(LCustomer.Active); Assert.AreEqual(1250.75, LCustomer.Balance, 0.01);end;
The mapping is by field name — the record field Name maps to the scenario field 'Name'. The match is case-insensitive. If a scenario field is missing for a record field, the record field keeps its default zero/empty value.
Materializing into Objects
AsObject<T> creates and populates a class instance by writing to its published properties:
type TCustomer = class private FName: string; FEmail: string; FAge: Integer; FActive: Boolean; FBalance: Double; published property Name: string read FName write FName; property Email: string read FEmail write FEmail; property Age: Integer read FAge write FAge; property Active: Boolean read FActive write FActive; property Balance: Double read FBalance write FBalance; end;procedure TTestCustomer.TestAliceObject;var LCustomer: TCustomer;begin LCustomer := TkbmMockRegistry.Get('customer_alice').AsObject<TCustomer>; try Assert.AreEqual('Alice Johnson', LCustomer.Name); Assert.AreEqual(34, LCustomer.Age); Assert.IsTrue(LCustomer.Active); finally LCustomer.Free; end;end;
The caller owns the returned object and must free it. The mapping uses published properties — the property name matches the scenario field name.
For classes with custom constructors, pass a factory function:
LCustomer := TkbmMockRegistry.Get('customer_alice') .AsObject<TCustomer>( function(AScenario: TkbmMockScenario): TCustomer begin Result := TCustomer.Create( AScenario.GetFieldValue('Name').AsString, AScenario.GetFieldValue('Age').AsInteger); end);
Loading Scenarios from JSON
Instead of defining every field in code, you can load scenarios from JSON:
// Inline JSONTkbmMockRegistry.Scenario('order_pending') .FromJSON('{' + '"OrderID": 1001,' + '"Customer": "Alice",' + '"Total": 299.99,' + '"Status": "Pending"' + '}');// From a fileTkbmMockRegistry.Scenario('order_shipped') .FromJSONFile('TestData\order_shipped.json');
You can also bulk-load multiple scenarios at once:
// Load all scenarios from a JSON fileTkbmMockRegistry.LoadFromFile('TestData\scenarios.json');
JSON support includes nested objects (mapped to TkbmMockNestedData) and automatic type inference for numbers, booleans, strings, and null values.
Loading from CSV
For tabular data, CSV is often the most natural format:
TkbmMockRegistry.Scenario('product_catalog') .FromCSVFile('TestData\products.csv');
The first row of the CSV is treated as field names. Each subsequent row becomes a row in a tabular scenario (see Part 5 for details on tabular data).
Capturing from Records and Objects
You can go the other direction too — capture data from an existing record or object into a scenario:
var LRec: TCustomerRec;begin LRec.Name := 'Test Customer'; LRec.Age := 25; LRec.Active := True; TkbmMockRegistry.Scenario('captured_customer') .FromRecord<TCustomerRec>(LRec);end;
Or from a live object:
TkbmMockRegistry.Scenario('captured_from_db') .FromObject<TCustomer>(LLoadedCustomer);
This is particularly useful for capturing test data from production databases during setup, then using it offline in tests.
RTTI Attribute Mapping
When field names in your scenario do not match your class properties, use RTTI attributes to control the mapping:
type TLegacyOrder = class private FOrderNumber: string; FDescription: string; FInternal: string; published [MockFieldName('order_id')] property OrderNumber: string read FOrderNumber write FOrderNumber; [MockFieldName('order_desc')] property Description: string read FDescription write FDescription; [MockFieldIgnore] property Internal: string read FInternal write FInternal; end;
| Attribute | Effect |
|---|---|
[MockFieldName('x')] | Maps the property to scenario field 'x' instead of the property name |
[MockFieldIgnore] | Skips the property entirely during mapping |
[MockFieldDefault(value)] | Uses value if the scenario field is missing |
[MockFieldRequired] | Raises an error if the scenario field is missing |
With the scenario:
TkbmMockRegistry.Scenario('legacy_order') .Field('order_id', 'ORD-2025-001') .Field('order_desc', 'Quarterly supplies') .Field('Internal', 'should_be_ignored');
Calling AsObject<TLegacyOrder> maps 'order_id' → OrderNumber, 'order_desc' → Description, and skips Internal.
Exporting Scenarios
Scenarios can be saved back to JSON or CSV:
TkbmMockRegistry.Get('customer_alice').SaveToJSONFile('alice.json');
This is useful for generating test fixture files that can be checked into version control.
Summary
This part introduced the mock data system:
- Named scenarios registered on
TkbmMockRegistrywith a fluent builder. - Field access via
GetFieldValue,TryGetFieldValue,HasField,FieldNames. - Materialization into records (
AsRecord<T>), objects (AsObject<T>), and custom factories. - Loading from JSON strings, JSON files, and CSV files.
- Capturing from existing records and objects.
- RTTI attributes for custom field mapping:
[MockFieldName],[MockFieldIgnore],[MockFieldDefault],[MockFieldRequired]. - Exporting back to JSON and CSV.
In Part 5 we will explore advanced mocking: scenario inheritance, tabular data (multi-row scenarios), value generators, AsDataSet materialization, and the mock data wizard.
![]()






