The Complete Guide to kbmUnitTest

This is part 4 of a 6-part series. Parts 1–3 covered the test framework itself.


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:

UnitPurpose
kbmUnitTest.MockCore: registry, scenarios, builder, materialization
kbmUnitTest.Mock.GeneratorsValue generators: Gen.IntRange, Gen.Sequential, etc.
kbmUnitTest.Mock.DataSetBridge 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;
interface
implementation
uses
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:

  1. Open — During initialization sections, scenarios are registered. Scenario() calls work.
  2. Sealed — Once TkbmMockRegistry.Seal is called, registration is closed and Get() / TryGet() calls work. The test runner calls Seal automatically, but you can call it explicitly in your initialization if tests and scenario registration are in the same unit.
  3. Complete — After all tests finish, Complete is 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 = class
public
[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 JSON
TkbmMockRegistry.Scenario('order_pending')
.FromJSON('{' +
'"OrderID": 1001,' +
'"Customer": "Alice",' +
'"Total": 299.99,' +
'"Status": "Pending"' +
'}');
// From a file
TkbmMockRegistry.Scenario('order_shipped')
.FromJSONFile('TestData\order_shipped.json');

You can also bulk-load multiple scenarios at once:

// Load all scenarios from a JSON file
TkbmMockRegistry.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;
AttributeEffect
[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 TkbmMockRegistry with 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.

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.