The Complete Guide to kbmUnitTest

This is part 5 of a 6-part series. Part 4 introduced the mock data system basics.


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

GeneratorDescriptionExample output
Gen.Sequential(start, step)Incrementing integer1, 2, 3, …
Gen.Sequential64(start, step)Incrementing Int641000000000, …
Gen.IntRange(min, max)Random integer in range42
Gen.Int64Range(min, max)Random Int64 in range9876543210
Gen.FloatRange(min, max)Random double in range73.42
Gen.RandomBool(probability)Random booleanTrue (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.GUIDRandom GUID string‘{A1B2C3D4-…}’
Gen.DateRange(from, to)Random date in range2025-03-15
Gen.DateRelative(daysOffset)Date relative to today2024-06-15
Gen.Fixed(value)Always the same value42
Gen.NullValueAlways 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:

  1. Parse type declarations — Paste a Delphi record or class declaration, click “Parse Type Declaration”, and the wizard extracts field names and types automatically.
  2. Edit fields manually — Add, remove, rename, and retype fields in a grid. Assign generators to fields for random data.
  3. Capture from database — Connect to a database, run a query, and import the result set as scenario rows.
  4. Define multiple scenarios — The left panel lists scenarios; you can create, duplicate, and organize them.
  5. Set inheritance — Use the “Inherits” dropdown to make one scenario inherit from another.
  6. Generate code — Click “Generate” to produce a .pas unit containing the scenario registrations and a .json fixture file.
  7. 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 / .EndRow for multi-row scenarios.
  • List materializationAsList<T> and AsObjectList<T>.
  • TDataSet materializationAsDataSet for seamless database testing.
  • Value generatorsGen.* for random, sequential, and pattern-based data.
  • Seeded randomnessSetSeed for reproducible tests.
  • TDataSet captureFromDataSet for 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.

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.