The Complete Guide to kbmUnitTest
This is part 5 of a 6-part series. Part 4 introduced the mock data system basics.
Contents
Scenario Inheritance
Real test suites often need variations on a theme: a “standard customer”, a “VIP customer” that inherits all standard fields but overrides the discount tier, and a “suspended customer” that inherits from standard but changes the status. kbmUnitTest’s mock data system supports single-inheritance between scenarios with .InheritsFrom():
initialization // Base scenario TkbmMockRegistry.Scenario('customer_standard') .Field('Name', 'Jane Doe') .Field('Email', 'jane@example.com') .Field('Age', 30) .Field('Active', True) .Field('DiscountTier', 'None') .Field('CreditLimit', 5000.00); // VIP inherits everything, overrides two fields TkbmMockRegistry.Scenario('customer_vip') .InheritsFrom('customer_standard') .Field('DiscountTier', 'Gold') .Field('CreditLimit', 50000.00); // Suspended inherits everything, overrides Active TkbmMockRegistry.Scenario('customer_suspended') .InheritsFrom('customer_standard') .Field('Active', False);
When you call TkbmMockRegistry.Get('customer_vip'), the registry resolves the inheritance chain: customer_vip inherits from customer_standard, so it has all six fields. Fields defined directly on the child override the parent.
Inheritance chains can be multiple levels deep:
TkbmMockRegistry.Scenario('customer_vip_premium')
.InheritsFrom('customer_vip')
.Field('DiscountTier', 'Platinum')
.Field('PersonalManager', 'John');
This scenario has all fields from customer_standard (via customer_vip), with DiscountTier overridden twice (final value: 'Platinum'), CreditLimit from customer_vip (50000.00), and a new field PersonalManager.
Circular inheritance is detected and raises EkbmMockCircularInheritance.
Tabular Data — Multi-Row Scenarios
Many tests need more than a single record. Product catalogs, order line items, and query results are naturally tabular. Use .AddRow / .EndRow to define multiple rows:
TkbmMockRegistry.Scenario('product_catalog')
.AddRow
.Field('ProductID', 1)
.Field('Name', 'Widget')
.Field('Price', 10.50)
.Field('InStock', True)
.EndRow
.AddRow
.Field('ProductID', 2)
.Field('Name', 'Gadget')
.Field('Price', 25.00)
.Field('InStock', True)
.EndRow
.AddRow
.Field('ProductID', 3)
.Field('Name', 'Doohickey')
.Field('Price', 5.99)
.Field('InStock', False)
.EndRow;
Access rows in your test:
procedure TTestCatalog.TestProductCount;var LCatalog: TkbmMockScenario;begin LCatalog := TkbmMockRegistry.Get('product_catalog'); Assert.AreEqual(3, LCatalog.RowCount);end;procedure TTestCatalog.TestFirstProduct;var LRow: TkbmMockScenario;begin LRow := TkbmMockRegistry.Get('product_catalog').Row(0); Assert.AreEqual('Widget', LRow.GetFieldValue('Name').AsString); Assert.AreEqual(10.50, LRow.GetFieldValue('Price').AsExtended, 0.01);end;
Row(i) returns a TkbmMockScenario representing a single row, so it supports all the same operations — GetFieldValue, AsRecord<T>, etc.
Materializing Lists from Tabular Data
AsList<T> converts all rows into a typed array of records:
type TProductRec = record ProductID: Integer; Name: string; Price: Double; InStock: Boolean; end;procedure TTestCatalog.TestAllProducts;var LProducts: TArray<TProductRec>;begin LProducts := TkbmMockRegistry.Get('product_catalog') .AsList<TProductRec>; Assert.AreEqual(3, Length(LProducts)); Assert.AreEqual('Widget', LProducts[0].Name); Assert.AreEqual('Doohickey', LProducts[2].Name); Assert.IsFalse(LProducts[2].InStock);end;
AsObjectList<T> does the same for class instances:
var LProducts: TObjectList<TProduct>;begin LProducts := TkbmMockRegistry.Get('product_catalog') .AsObjectList<TProduct>; try Assert.AreEqual(3, LProducts.Count); Assert.AreEqual('Gadget', LProducts[1].Name); finally LProducts.Free; // frees all contained objects end;end;
Materializing as TDataSet
Add kbmUnitTest.Mock.DataSet to your uses clause and call AsDataSet on any scenario. It returns a TClientDataSet with fields inferred from the scenario’s value types. You can use it directly for assertions, or copy its data into a TkbmMemTable or kbmMW dataset if your code under test expects one of those:
uses kbmUnitTest.Mock, kbmUnitTest.Mock.DataSet, kbmTestDataSet;procedure TTestCatalog.TestAsDataSet;var LDS: TClientDataSet;begin LDS := TkbmMockRegistry.Get('product_catalog').AsDataSet; try ThatDataSet(LDS) .IsActive .HasRowCount(3) .HasFields(['ProductID', 'Name', 'Price', 'InStock']) .AtRow(0) .FieldEquals('Name', 'Widget') .FieldEquals('Price', 10.50, 0.01) .AtRow(2) .FieldEquals('InStock', False); finally LDS.Free; end;end;
This is a powerful combination: define your test data as a scenario, materialize it as a dataset, then use ThatDataSet fluent assertions to verify the output of code that processes datasets. The entire pipeline — data definition, materialization, and verification — is fluent and readable.
Single-record (non-tabular) scenarios produce a TClientDataSet with one row. Since TClientDataSet, TkbmMemTable, and kbmMW datasets all descend from TDataSet, the ThatDataSet fluent assertions from Part 3 work identically on any of them.
Value Generators
Generators produce fresh values every time they are called. This is useful for fuzz testing, boundary testing, and generating realistic-looking data without hard-coding every value. The Gen class is a static factory:
uses kbmUnitTest.Mock, kbmUnitTest.Mock.Generators;initialization TkbmMockRegistry.Scenario('random_user') .Field('ID', Gen.Sequential(1)) .Field('Name', Gen.OneOf(['Alice', 'Bob', 'Charlie', 'Diana'])) .Field('Email', Gen.StringPattern('AAAA.AAAA@example.com')) .Field('Age', Gen.IntRange(18, 80)) .Field('Score', Gen.FloatRange(0.0, 100.0)) .Field('IsActive', Gen.RandomBool(0.8)) .Field('CreatedAt', Gen.DateRelative(-365)) .Field('Token', Gen.GUID);
Each time you access a generator-backed field, you get a new value:
var S: TkbmMockScenario;begin S := TkbmMockRegistry.Get('random_user'); // Each call generates a new value WriteLn(S.GetFieldValue('ID').AsInteger); // 1 WriteLn(S.GetFieldValue('ID').AsInteger); // 2 WriteLn(S.GetFieldValue('ID').AsInteger); // 3 WriteLn(S.GetFieldValue('Name').AsString); // 'Charlie' (random)end;
Generator Reference
| Generator | Description | Example output |
|---|---|---|
Gen.Sequential(start, step) | Incrementing integer | 1, 2, 3, … |
Gen.Sequential64(start, step) | Incrementing Int64 | 1000000000, … |
Gen.IntRange(min, max) | Random integer in range | 42 |
Gen.Int64Range(min, max) | Random Int64 in range | 9876543210 |
Gen.FloatRange(min, max) | Random double in range | 73.42 |
Gen.RandomBool(probability) | Random boolean | True (80% chance) |
Gen.OneOf([...]) | Random pick from strings | ‘active’ |
Gen.OneOfValues([...]) | Random pick from TValue array | … |
Gen.StringOfLength(n) | Random chars, exact length | ‘xKp9mQw2rT’ |
Gen.StringPattern(pat) | Pattern: A=letter, 0=digit, ?=any | ‘AB12-CD34’ |
Gen.LoremIpsum(wordCount) | Pseudo-Latin placeholder text | ‘lorem ipsum dolor…’ |
Gen.GUID | Random GUID string | ‘{A1B2C3D4-…}’ |
Gen.DateRange(from, to) | Random date in range | 2025-03-15 |
Gen.DateRelative(daysOffset) | Date relative to today | 2024-06-15 |
Gen.Fixed(value) | Always the same value | 42 |
Gen.NullValue | Always TValue.Empty | (empty) |
Gen.Computed(func) | Custom lambda called each time | (anything) |
Seeded Randomness
For reproducible tests, set a global seed before scenarios are registered:
initialization TkbmMockRegistry.SetSeed(12345); TkbmMockRegistry.Scenario('reproducible_data') .Field('Score', Gen.IntRange(0, 100));
With the same seed, the same sequence of random values is produced every run. This makes failing tests reproducible even when they use random data.
Mixing Static and Generated Fields
A scenario can have both static values and generators:
TkbmMockRegistry.Scenario('order')
.Field('OrderID', Gen.Sequential(1000)) // generated
.Field('Currency', 'EUR') // static
.Field('Customer', 'Alice') // static
.Field('Total', Gen.FloatRange(10.0, 500.0)) // generated
.Field('CreatedAt', Gen.DateRelative(-30)); // generated
Static fields always return the same value. Generator fields produce a new value on each access.
Capturing from TDataSet
The FromDataSet builder extension lets you capture live data from any TDataSet descendant — a SQL query, a TkbmMemTable, a kbmMW dataset, or anything else — into a scenario:
uses kbmUnitTest.Mock, kbmUnitTest.Mock.DataSet;initialization // Assume FQuery is an open TDataSet pointing at real data TkbmMockRegistry.Scenario('captured_orders') .FromDataSet(FQuery, 10); // capture up to 10 rows
This is useful during development: run the app against a real database, capture representative data, then export it to JSON for offline use in tests:
TkbmMockRegistry.Scenario('captured_orders')
.FromDataSet(FQuery, 10)
.Build
.SaveToJSONFile('TestData\orders.json');
In your test project, load from the JSON file instead:
TkbmMockRegistry.Scenario('captured_orders')
.FromJSONFile('TestData\orders.json');
The Mock Data Wizard
kbmUnitTest ships with an IDE wizard for visual scenario creation. Open it via File → New → Other → kbmUnitTest → kbmUnitTest – Mock Data Wizard or Tools → kbmUnitTest Mock Data Wizard.
The wizard lets you:
- Parse type declarations — Paste a Delphi record or class declaration, click “Parse Type Declaration”, and the wizard extracts field names and types automatically.
- Edit fields manually — Add, remove, rename, and retype fields in a grid. Assign generators to fields for random data.
- Capture from database — Connect to a database, run a query, and import the result set as scenario rows.
- Define multiple scenarios — The left panel lists scenarios; you can create, duplicate, and organize them.
- Set inheritance — Use the “Inherits” dropdown to make one scenario inherit from another.
- Generate code — Click “Generate” to produce a
.pasunit containing the scenario registrations and a.jsonfixture file. - Preview — See the generated code before saving.
The wizard is a productivity tool for creating the initial dataset; once generated, you maintain the code or JSON files as part of your project.
Summary
This part covered the advanced features of the mock data system:
- Inheritance —
.InheritsFrom()for scenario variations with override semantics. - Tabular data —
.AddRow/.EndRowfor multi-row scenarios. - List materialization —
AsList<T>andAsObjectList<T>. - TDataSet materialization —
AsDataSetfor seamless database testing. - Value generators —
Gen.*for random, sequential, and pattern-based data. - Seeded randomness —
SetSeedfor reproducible tests. - TDataSet capture —
FromDataSetfor importing live data. - Mock Data Wizard — IDE integration for visual scenario creation.
In Part 6 we will tie everything together: combining unit tests with mock data, using the Publish/Require pattern for test dependencies, diagnostics, and building a complete CI/CD pipeline.
![]()






