MiniSpec - User Guide
π Language: English | EspaΓ±ol
Welcome to the MiniSpec guide, the BDD framework for Delphi.
π See also: Testing Patterns β How to use BDD for unit, integration, and E2E tests.
Table of Contents
Getting Started
- Getting Started β Why MiniSpec? Your first specification. Running specs.
Fundamental Concepts
- The World β Sharing state between scenario steps
- Scenario Outline β Multiple examples with data tables
- DataTables β Structured data for complex steps
Organization and Reuse
- Step Bindings β Reusable steps with regex patterns
- Tags and Filtering β Categorize and filter scenarios
- Rules β Group scenarios by business rule
Verifications
- Assertions (Expect) β Fluent API for verifications and exceptions
Vocabulary and Markers
- Gherkin Vocabulary β Complete keyword reference
- Pending and NoAction β Unimplemented or descriptive steps
Advanced Configuration
- Hooks (Before/After) β Feature-level setup/teardown code
- Global Configuration β MiniSpec, SpecContext, FeatureContext
- Dependency Injection β Built-in [MiniInject] system
Execution and Output
- Reporters β Console, JSON, JUnit, Gherkin, Live Dashboard
- Command Line β Execution options
- Nested Runs β
TSpecRun: a suite run inside another one
Quick Reference
| Concept | Usage |
|---|---|
| Feature | Feature('...') β Functionality to specify |
| Scenario | .Scenario('...') β Concrete example |
| Given/When/Then | .Given(), .When(), .&Then() β Steps |
| ScenarioOutline | .ScenarioOutline('...').Examples([...]) β Data-driven |
| Background | .Background β Common setup |
| Rule | .Rule('...') β Group by rule |
| Tags | @tag in description β Filtering |
| World | .UseWorld<T> β Scenario state |
Getting Started
Getting Started with MiniSpec
π Language: English | EspaΓ±ol
Why MiniSpec?
Imagine you could write your application requirements in a way that:
- Anyone can read them β developers, testers, analysts, clients
- They run automatically β verifying the code meets the specification
- They never get outdated β because they ARE the test
This idea has a name: Specification by Example. Instead of requirement documents that nobody reads and become stale after the first change, you write concrete examples of expected behavior. These examples become executable specifications: documentation that validates itself automatically.
This practice is the heart of BDD (Behavior-Driven Development), a widely adopted methodology for capturing requirements collaboratively. The vocabulary BDD uses is called Gherkin and youβll recognize it by its keywords: Feature, Scenario, Given, When, Then. Tools like Cucumber, SpecFlow, and Behave have been demonstrating the value of this approach for years.
MiniSpec brings BDD to Delphi, using Gherkin vocabulary natively:
- Feature: The functionality youβre specifying
- Scenario: A concrete example of how it should behave
- Given-When-Then: The pattern that structures each example
The result is code that documents, specifies, and verifies at the same time.
Your First Specification
Imagine youβre developing a calculator. Before writing code, you describe how it should behave:
βWhen I add 2 and 3, the result should be 5β
This simple sentence is an executable specification. With MiniSpec, you write it almost literally:
unit Addition.Feat;
interface
implementation
uses
Daf.MiniSpec,
Calculator.Engine; // Here's TCalculator, the system we're specifying
type
TCalculatorWorld = class
public
Calculator: TCalculator; // The SUT (System Under Test)
destructor Destroy; override;
end;
destructor TCalculatorWorld.Destroy;
begin
Calculator.Free;
inherited;
end;
initialization
Feature('Calculator - Addition')
.UseWorld<TCalculatorWorld>
.Background
.Given('I have a calculator', procedure(Ctx: TCalculatorWorld)
begin
Ctx.Calculator := TCalculator.Create;
end)
.Scenario('Add two positive numbers')
.When('I add 2 and 3', procedure(Ctx: TCalculatorWorld)
begin
Ctx.Calculator.Add(2, 3);
end)
.&Then('the result is 5', procedure(Ctx: TCalculatorWorld)
begin
Expect(Ctx.Calculator.Result).ToEqual(5);
end)
end.The specification defines the behavior of TCalculator β the SUT (System Under Test). We donβt care how itβs implemented, only what it should do. The implementation is Calculator.Engine.pas, from the samples/CalculatorSpecs/ sample:
unit Calculator.Engine;
interface
uses
System.SysUtils;
type
TCalculator = class
strict private
FResult: Integer;
public
procedure Add(A, B: Integer);
procedure Subtract(A, B: Integer);
procedure Mult(A, B: Integer);
procedure Divide(A, B: Integer);
property Result: Integer read FResult;
end;
implementation
procedure TCalculator.Add(A, B: Integer);
begin
FResult := A + B;
end;
procedure TCalculator.Subtract(A, B: Integer);
begin
FResult := A - B;
end;
procedure TCalculator.Mult(A, B: Integer);
begin
FResult := A * B;
end;
procedure TCalculator.Divide(A, B: Integer);
begin
if B = 0 then
raise EDivByZero.Create('Division by zero');
FResult := A div B;
end;
end.Whatβs happening here?
- Feature declares what functionality weβre specifying
- UseWorld defines a class containing the SUT and test data
- Background prepares common context for all scenarios (creates the calculator)
- Scenario describes a concrete use case
- When executes the action we want to test (uses the SUT)
- Then verifies the result is as expected
π‘ Steps follow the Given-When-Then pattern: βGiven thatβ¦ Whenβ¦ Thenβ¦β. This simple but powerful pattern forces you to think about preconditions, actions, and results.
Running the Specification
To make the specification executable, you need a minimal program. Together with the feature above, it is the samples/FirstSpec/ sample:
program FirstSpec;
{$APPTYPE CONSOLE}
uses
Daf.MiniSpec,
Calculator.Engine in '..\CalculatorSpecs\Calculator.Engine.pas',
Addition.Feat in 'Addition.Feat.pas';
begin
MiniSpec.Run;
end.When running, youβll see in the console:
Feature: Calculator - Addition
Background:
β Given I have a calculator (0 ms)
Scenario: Add two positive numbers
β When I add 2 and 3 (0 ms)
β Then the result is 5 (2 ms)
Passed: 1 | Failed: 0 | Pending: 0 | Undefined: 0 | Skipped: 0 | Total: 1 Specs in 1 Features | 0 ms | at 2026-09-30T11:15:11
The cycle is simple: describe expected behavior β run β MiniSpec tells you if the code meets the specification.
β Back to Guide | Next: The World β
The World
The World: Sharing State
π Language: English | EspaΓ±ol
The World is the glue between scenario steps. Each step receives the same instance, allowing:
- Given prepares initial state
- When is the event, one and named, that exercises the SUT over that state
- Then verifies the observable outcome
MiniSpec creates and destroys the World automatically for each scenario. Thatβs why your World class must have a parameterless constructor (the default TObject constructor is sufficient). If you need to free resources, override Destroy:
type
TCalculatorWorld = class
public
Calculator: TCalculator; // The object under test
destructor Destroy; override;
end;
destructor TCalculatorWorld.Destroy;
begin
Calculator.Free;
inherited;
end;Each scenario receives a new World. This ensures scenarios are isolated from each other: what one scenario does doesnβt affect others.
Feature('...')
.UseWorld<TCalculatorWorld> // MiniSpec creates one instance per scenario
.Scenario('First test') // World #1
// ...
.Scenario('Second test') // World #2 (independent from previous)
// ...β Getting Started | Next: Scenario Outline β
Scenario Outline
Multiple Examples with Scenario Outline
π Language: English | EspaΓ±ol
A single example is rarely enough. What about negative numbers? What about zero? You could write several almost identical scenarios⦠or use Scenario Outline:
.ScenarioOutline('Adding <A> and <B> should give <Result>')
.When('I add <A> and <B>', procedure(Ctx: TCalculatorWorld)
begin
Ctx.Calculator.Add(Ctx.A, Ctx.B);
end)
.&Then('the result is <Result>', procedure(Ctx: TCalculatorWorld)
begin
Expect(Ctx.Calculator.Result).ToEqual(Ctx.Result);
end)
.Examples([
['A', 'B', 'Result'], // Headers = World field names
[ 2, 3, 5], // Example 1
[ 0, 0, 0], // Example 2
[-1, 1, 0], // Example 3
[100, -50, 50] // Example 4
])Whatβs happening here?
- The placeholders
<A>,<B>,<Result>in descriptions are replaced with concrete values - MiniSpec reads values from the Examples table and injects them into corresponding World properties (thatβs why World must have
A,B, andResultproperties) - The scenario runs once per row in the Examples table, each with a new World (like any normal scenario)
The output shows the outline with its examples table:
Feature: Calculator - Addition
Background:
β Given I have a calculator (0 ms)
Scenario Outline: Adding <A> and <B> should give <Result>
When I add <A> and <B>
Then the result is <Result>
Examples:
| A | B | Result |
β | 2 | 3 | 5 | (0 ms)
β | 0 | 0 | 0 | (0 ms)
β | -1 | 1 | 0 | (0 ms)
β | 100 | -50 | 50 | (0 ms)
Passed: 4 | Failed: 0 | Pending: 0 | Undefined: 0 | Skipped: 0 | Total: 4 Specs in 1 Features | 0 ms | at 2026-01-30T14:57:07
π‘ A normal Scenario is simply an Outline with a single implicit example. Thereβs no conceptual difference: both specify behavior with concrete examples. The Outline just makes the data table explicit.
Display-only steps
A step defined without a lambda is display-only: it appears in the report but runs as a no-op. This is useful for the Given row in outlines where the data already comes from the Examples table via RTTI injection β no code needed:
.ScenarioOutline('Adding <A> and <B> should give <Result>')
.Given('the numbers <A> and <B>') // display-only β values injected from Examples
.When('they are added', procedure(Ctx: TCalculatorWorld)
begin
Ctx.Result := Ctx.A + Ctx.B;
end)
...The Given line appears in the output as context but does not execute any code. If a step binding matching the substituted description is registered, it runs instead.
Pending steps, and an outline without Examples
A step declared .Pending in the outline is pending in every row: no row runs it or the steps after it, and each row counts as Pending.
An outline with no .Examples runs once, as a scenario: with its World and its Background, counted as one scenario and printed with its steps. There a step without a lambda is not display-only: with no binding that matches it, it is Undefined, as in a Scenario.
β The World | Next: DataTables β
DataTables
DataTables: Structured Data
π Language: English | EspaΓ±ol
Sometimes a step needs more complex data than simple values. DataTables allow passing tabular structures:
.Scenario('Create multiple users')
.Given('the following users:', [
['name', 'email', 'role'],
['Alice', 'alice@test.com', 'admin'],
['Bob', 'bob@test.com', 'user'],
['Carol', 'carol@test.com', 'user']
], procedure(World: TMyWorld)
begin
var Table := SpecContext.DataTable;
for var I := 0 to Table.RowCount - 1 do
begin
var Row := Table.AsMap(I); // Row as dictionary
World.Input.Add( // input data, not yet processed
Row['name'].AsString,
Row['email'].AsString,
Row['role'].AsString
);
end;
end)
.When('I import them into the directory', procedure(World: TMyWorld)
begin
World.Directory.Import(World.Input); // the SUT: the event happens here
end)
.&Then('the directory has 3 users', procedure(World: TMyWorld)
begin
Expect(World.Directory.Count).ToEqual(3);
end)DataTable API
| Property/Method | Description |
|---|---|
RowCount |
Number of rows (not counting headers) |
ColCount |
Number of columns |
Headers |
Array with column names |
Cell[row, col] |
Cell by indices |
Cell[row, 'name'] |
Cell by column name |
AsMap(row) |
Row as TDictionary<string, TValue> |
AsList |
All rows as array of dictionaries |
Transpose |
New table with rows/columns swapped |
β Scenario Outline | Next: Step Bindings β
Step Bindings
Step Bindings: Organizing Complex Steps
π Language: English | EspaΓ±ol
As your specifications grow, youβll notice many steps repeat:
// In Feature A:
.Given('an authenticated user', procedure(W: TWorldA) begin ... end)
// In Feature B:
.Given('an authenticated user', procedure(W: TWorldB) begin ... end) // Duplicate!Step Bindings solve this: define steps as class methods, using regex patterns:
unit Auth.Steps;
interface
uses
Daf.MiniSpec,
Daf.MiniSpec.Binding;
type
TAuthBindings = class
public
[Given('a user "([^"]+)" with password "([^"]+)"')]
procedure SetupUser(World: TObject; Username, Password: string);
[When('logs in')]
procedure DoLogin(World: TObject);
[ThenAttribute('login is successful')]
procedure VerifyLoginSuccess(World: TObject);
[ThenAttribute('login fails with "([^"]+)"')]
procedure VerifyLoginError(World: TObject; ExpectedError: string);
end;
implementation
procedure TAuthBindings.SetupUser(World: TObject; Username, Password: string);
begin
var W := World as TAuthWorld;
W.Username := Username;
W.Password := Password;
end;
// ... rest of implementationsRegister and Use
initialization
Bindings.RegisterSteps<TAuthBindings>;
Feature('Authentication')
.UseWorld<TAuthWorld>
.Scenario('Valid login')
.Given('a user "admin" with password "secret123"') // Uses the binding
.When('logs in')
.&Then('login is successful')
.Scenario('Wrong password')
.Given('a user "admin" with password "wrong"')
.When('logs in')
.&Then('login fails with "Invalid credentials"')Binding Characteristics
| Aspect | Description |
|---|---|
| Patterns | Regex with capture groups for parameters |
| Types | Integer, Int64, Float, string, Boolean |
| First parameter | Always the World (use TObject and cast) |
| Priority | Inline lambda > Registered binding |
| Attributes | [GivenAttribute], [WhenAttribute], [ThenAttribute] |
π‘ Bindings are ideal for common steps (authentication, data setup, etc.) used across multiple features.
β DataTables | Next: Tags and Filtering β
Rules: Grouping by Business Rule
π Language: English | EspaΓ±ol
When a Feature has multiple business rules, Rules help organize the scenarios that illustrate each rule:
Feature('Discount System')
.UseWorld<TDiscountWorld>
.Rule('VIP customers get 20% discount')
.Scenario('Normal purchase by VIP customer')
.Given('a VIP customer', procedure(W: TDiscountWorld)
begin
W.Customer := TCustomer.Create(TCustomerType.VIP);
end)
.When('buying a $100 product', procedure(W: TDiscountWorld)
begin
W.Purchase := W.Customer.Buy(100);
end)
.&Then('the total is $80', procedure(W: TDiscountWorld)
begin
Expect(W.Purchase.Total).ToEqual(80);
end)
.Rule('Purchases over $100 have free shipping')
.Scenario('Purchase of $150')
// ...
.Scenario('Purchase of $50')
// ...Rule Behavior
| Aspect | Description |
|---|---|
| Grouping | Scenarios under a Rule share that business rule |
| Non-nestable | Each .Rule() automatically closes the previous one |
| Background | The Featureβs Background applies to all scenarios, including those in Rules |
| Filtering | Use Rule:text to filter by rule description |
EndRule: Scenarios without Rule
.EndRule is only needed when you want to add scenarios without Rule after a Rule:
Feature('...')
.Rule('A specific rule')
.Scenario('Test inside the Rule')
.EndRule // Needed to exit the Rule
.Scenario('General scenario without Rule')β Assertions | Next: Gherkin Vocabulary β
Assertions (Expect)
Assertions (Expect)
π Language: English | EspaΓ±ol
MiniSpec provides a fluent API for verifications:
// Values
Expect(result).ToEqual(5);
Expect(value).ToNotEqual(0);
Expect(number).ToBeBetween(1, 10);
// Strings
Expect(message).ToContain('error');
Expect(email).ToMatch('^[\w.-]+@[\w.-]+\.\w+$');
// Booleans and nulls
Expect(active).ToBeTrue;
Expect(obj).ToNotBeNull;Value matchers
Every one of them is on the record Expect returns. They compare through a Variant, so the overload you call decides nothing about the comparison.
| Matcher | Passes when |
|---|---|
ToEqual(V) / ToEqual<T>(V) |
The value equals V |
ToNotEqual(V) / ToNotEqual<T>(V) |
It does not |
ToBeGreaterThan(V) ToBeGreaterOrEqual(V) |
Ordered comparison |
ToBeLessThan(V) ToBeLessOrEqual(V) |
The same, the other way |
ToBeBetween(Min, Max) |
Min <= value <= Max, both ends included |
ToBeCloseTo(V, Tolerance) |
Abs(value - V) <= Tolerance. Tolerance defaults to 0.0001 |
ToBeWithinPercent(V, Percent) |
The same, with the tolerance as a percentage of V |
ToBeTrue / ToBeFalse |
The value is that boolean |
ToBeNull / ToNotBeNull |
The variant is Null, or is not |
ToBeEmpty |
The variant is Empty, or its string form is '' |
ToContain(S) |
The string form contains S, ignoring case |
ToStartWith(S) / ToEndWith(S) |
It starts or ends with S, also ignoring case |
ToMatch(Pattern) |
TRegEx.IsMatch over the string form β a regular expression, not a glob |
ToHaveCount(N) |
The value is a variant array with N elements. Anything else fails with Expected array value |
β οΈ ToBeEmpty does not print the value it found. Its failure says Expected value to be empty and no more, so it cannot name what was there. When the failure has to name something, assert with ToEqual against a sentinel instead.
Verifying Exceptions
An important special case: verifying that code throws an exception. MiniSpec automatically captures exceptions in the When step, allowing verification in Then:
.Scenario('Division by zero')
.Given('the numbers 10 and 0', procedure(World: TWorld)
begin
World.A := 10;
World.B := 0;
end)
.When('dividing', procedure(World: TWorld)
begin
World.Calculator.Divide(World.A, World.B); // Exception captured
end)
.&Then('raises EDivByZero', procedure(World: TWorld)
begin
ExpectException(Raised).ToBe(EDivByZero);
end);Exception Methods
| Method | Description |
|---|---|
ExpectException(Raised).ToBeAny |
Any exception was raised |
ExpectException(Raised).ToBe(EMyException) |
Specific type was raised |
ExpectException(Raised).ToBe<EMyException> |
The same, written as a type argument |
ExpectException(Raised).ToHaveMessage('text') |
Message contains substring |
ExpectException(Raised).ToBeNone |
No exception was raised |
ExpectException(Raised).ToMatchMessage('re') |
Message matches a regular expression |
Raised is the exception that escaped the When step. ExpectException also takes the call itself, for a check that is not the scenarioβs event:
ExpectException(
procedure
begin
World.Calculator.Divide(10, 0);
end).ToBe<EDivByZero>;β οΈ ToMatchMessage is on TExpectException, not on TExpect: calling it on a plain value gives E2003 Undeclared identifier. And it is a regex, so '*Between*2*1*' matches nothing where 'Between.*2.*1' matches.
β Tags and Filtering | Next: Rules β
Gherkin Vocabulary
Gherkin Vocabulary
π Language: English | EspaΓ±ol
Complete reference of Gherkin vocabulary in MiniSpec:
| Keyword | MiniSpec | Description |
|---|---|---|
| Feature | Feature('...') |
Functionality being specified |
| Rule | .Rule('...') |
Groups scenarios under a business rule |
| Background | .Background |
Steps common to all scenarios; if one fails, the scenario fails without running its own steps |
| Scenario | .Scenario('...') |
A concrete example of behavior |
| Scenario Outline | .ScenarioOutline('...') |
Parameterized example with table |
| Examples | .Examples([...]) |
Data table for the Outline |
| DataTable | After step description | Structured data table for a step |
| Given | .Given('...', proc) |
Establishes initial context |
| When | .When('...', proc) |
The action being tested |
| Then | .&Then('...', proc) |
The expected result (& required) |
| And | .&And('...', proc) |
Additional step (& required) |
| But | .But('...', proc) |
Exception or negation |
| @tag | In description | For filtering scenarios |
π‘ The
&beforeThenandAndis required because they are reserved words in Delphi.
What each phase claims
A scenario states the behaviour of one event. The three phases are that claim, split up β so each one has a job, and a piece that does not belong to it:
| Phase | Claims | Does not do |
|---|---|---|
Given |
the state the event needs to make sense | trigger the event |
When |
the event, one and named | verify anything |
Then |
what can be observed afterwards | trigger anything |
The test, scenario by scenario: name the event. Which actionβs behaviour am I claiming here? If you can name it, it belongs in the When. If you cannot, the scenario has no subject β and that is not fixed by moving the code to another phase.
β οΈ A scenario whose event sits outside the When still passes and fails, so nothing turns red to tell you. What you lose instead is this:
- Event in the
Givenβ a failure of the event is reported as a setup failure, sending the reader to the wrong place. And theThencan only compare what theGivenleft behind, so the assertion measures the setup rather than the subject. - Event in the
Thenβ the failure is reported as a failed assertion. With severalThensteps, either each one repeats the event β making them different events rather than one observed twice β or only the first runs it.
See Pending and NoAction for what it means to leave a phase empty.
Features with Narrative
A Feature can include a narrative that explains the why:
Feature('''
Calculator - Division
As a calculator user
I need to divide numbers
To solve mathematical problems
@math @division
''')The first line is the title. MiniSpec takes the rest as narrative. Tags can appear anywhere in the description but are recommended at the end.
Category for Filtering
Assign categories to filter with Cat:text. You can use a string or a class as marker:
// With string
Feature('Login')
.Category('auth')
.UseWorld<TLoginWorld>
// With marker class
type
TIntegrationTest = class end; // Empty class as marker
Feature('Database operations')
.Category(TIntegrationTest) // Uses QualifiedClassName: 'Unit1.TIntegrationTest'
.UseWorld<TDbWorld>Filter by category:
MyApp.exe -f "Cat:auth" # Features with category 'auth'
MyApp.exe -f "Cat:Integration" # Matches 'TIntegrationTest'π‘ Marker classes are useful for categories used across multiple files: the compiler catches typos.
β Rules | Next: Pending and NoAction β
Pending and NoAction
Pending and NoAction
π Language: English | EspaΓ±ol
MiniSpec provides two ways to mark steps that have no executable code:
| Method | Semantics | Result | Typical use |
|---|---|---|---|
.Pending |
βPending implementationβ | Pending + stops scenario | Work in progress |
.NoAction |
βNo action requiredβ | Pass + continues scenario | Purely descriptive steps |
Pending
Indicates a step is pending implementation. The scenario counts as Pending and following steps are not executed:
.Scenario('Feature in development')
.Given('a precondition', procedure(Ctx: TMyWorld) begin end)
.When('something pending implementation')
.Pending // Scenario Pending, doesn't continue
.&Then('never executes', procedure(Ctx: TMyWorld)
begin
// This code does not execute
end)A step of the Background works the same way: a .Pending step, or one with no binding, leaves the rest of the Background and the steps of the scenario unrun, and the scenario counts as Pending or Undefined, as if the step were its own.
NoAction
Marks a step as purely descriptive. It passes automatically and the following steps DO execute:
.Scenario('With a descriptive context')
.Given('the system is configured') // the environment already provides it
.NoAction
.When('user logs in', procedure(Ctx: TMyWorld)
begin
Ctx.DoLogin; // this code DOES execute
end)
.&Then('accesses dashboard', procedure(Ctx: TMyWorld)
begin
Expect(Ctx.IsLoggedIn).ToBeTrue; // also executes
end)The last resort
A scenario states the behaviour of one event, split across the three phases: Given the state the event needs, When the event, Then what can be observed afterwards. A step with no body says that piece is missing, and the scenario stops saying what it validates.
It is not forbidden β the marker exists for a reason β but its legitimate use is rare, and rarest of all on a When and on a Then, so it is justified when used rather than taken for granted.
The rule that decides: a step with nothing to run almost always has something to check. That is its form as a check, and it is what leaves .NoAction for the genuine remainder.
| Phase | What to do when there is nothing to run |
|---|---|
Given |
assert that the environment is what you assume |
When |
for an implicit action, wait until it has happened and assert that it did |
Then |
nothing fits here: a scenario that observes nothing asserts nothing |
So before writing it, two questions: is there anything to run here? and is there anything to check here? .NoAction is the answer when both are no.
The implicit action
Some events are produced by the system rather than by the scenario: a background task that runs on its own, a timer, something that already happened while a unit was initialising. The When names the event even though the test did not cause it.
.When('implicit action').NoActionTwo questions before accepting it, because most steps that look implicit are not:
- Can I trigger it? Then trigger it in the
When, and this is not the case. - Can I wait for it? Then the wait is the stepβs body:
.When('the background task has run',
procedure(W: TMyWorld)
begin
Expect(W.WaitForTask(5000)).ToBeTrue;
end)β οΈ A When that waits removes the race. Without it the Then may run before the task finishes, and the scenario then passes or fails depending on how loaded the machine is β which is flakiness, not a matter of form.
β οΈ Everywhere else, a When with no action still prints β, so the report declares an event that never happened, which reads as covered. If the scenario has no event at all, it has no behaviour under test: omitting the When instead of disabling it is the same situation with a different label. Where the event usually turns up is in the Given, which ran it too early, or inside the Then, which runs it while asserting.
β οΈ Nothing stops you from putting it on a Then, and nothing warns you. The step becomes a body that does nothing and passes, so the scenario keeps a Then that verifies nothing and stays green β worse than a failure, because it looks like coverage.
.&Then('verification').NoAction // compiles, runs, verifies nothingThe Given that has nothing to build
The legitimate case is real but narrow: the environment already holds the state, so there is nothing to set up. Even there, checking beats doing nothing β assert that the environment is what you assume:
.Given('the system is configured',
procedure(W: TMyWorld)
begin
Expect(W.Environment.IsReady).ToBeTrue;
end)A Given that asserts its starting point turns an environment failure into a red saying the environment was not what you assumed, instead of a failing Then that sends you looking in the wrong place.
Where it lands
.NoAction replaces the body of the last declared step, whatever its kind:
.Given('descriptive context').NoActionβ Gherkin Vocabulary | Next: Hooks β
Hooks (Before/After)
Before / After Hooks
π Language: English | EspaΓ±ol
Before and After hooks execute code once per Feature, unlike Background which executes per scenario:
Feature('Database Tests')
.UseWorld<TDbWorld>
.Before('Start test database', procedure
begin
GTestDb := TTestDatabase.Create;
GTestDb.Start;
end)
.After('Stop test database', procedure
begin
GTestDb.Free;
end)
.Background
.Given('a fresh transaction', procedure(W: TDbWorld)
begin
W.Tx := GTestDb.BeginTransaction; // Executes per scenario
end)
.Scenario('Insert record')
// ...Comparison
| Element | Executes | Typical use |
|---|---|---|
Before |
Once before all scenarios | Start server, create DB |
After |
Once after all scenarios | Free expensive resources |
Background |
Before each scenario | Prepare test data |
Note: Hooks donβt receive World because they execute before any instance exists.
Hooks of the suite
Before and After declared on MiniSpec, in the program that runs the suite, execute once before the first feature and once after the last one, also when a feature fails:
MiniSpec
.Before('Start the shared server', procedure
begin
// ...
end)
.After('Stop the shared server', procedure
begin
// ...
end)
.Run;When a hook raises
In a feature, a Before that raises stops the Before hooks after it, and the scenarios of the feature do not run. The feature counts as a failure, and the report shows the message under its heading. Its After hooks run all the same.
In the suite, every Before runs even when one raises, and then no feature runs. Every After runs too. Each hook that raises counts as one failure, its message appears at the start of the console report, and the run exits with 1.
β Pending and NoAction | Next: Configuration β
Global Configuration
Global Configuration
π Language: English | EspaΓ±ol
MiniSpec: Global Suite Configuration
The MiniSpec function returns the global test suite instance. It allows configuring global options and suite-level hooks (before/after all features):
program MySpecs;
{$APPTYPE CONSOLE}
uses
Daf.MiniSpec,
// ... features ...
begin
MiniSpec
.Category('My Test Suite') // Suite title
.Before('Initialize environment', procedure
begin
// Runs ONCE, before all features
DatabaseTestServer.Start;
end)
.After('Cleanup environment', procedure
begin
// Runs ONCE, after all features
DatabaseTestServer.Stop;
end);
MiniSpec.Run;
end.UseSuiteContext: Global State
Similar to UseFeatureContext but at suite level. Context is shared between all features:
type
TGlobalContext = class
public
TestServer: TTestServer;
SharedConfig: TConfig;
end;
begin
MiniSpec
.UseSuiteContext<TGlobalContext>
.Before('Start server', procedure
begin
// TGlobalContext is already created and available via injection
end);
MiniSpec.Run;
end.World classes from each feature can inject this context with [MiniInject].
Execution Options
MiniSpec
.DryRun(True) // List scenarios without executing
.Pause(True) // Wait for key at end
.Reporter('live:port=9000'); // Configure reporter programmaticallySpecContext: Execution Context Access
For advanced cases where you need access to execution context (current step, scenario, feature), MiniSpec provides the global SpecContext function.
type
TMyWorld = class // Simple class, no special inheritance required
public
Value: Integer;
end;
// In steps, access context via SpecContext:
.When('I execute something', procedure(World: TMyWorld)
begin
// Access current step
WriteLn('Step: ', SpecContext.Step.Description);
// Direct navigation to parents
WriteLn('Scenario: ', SpecContext.Scenario.Description);
WriteLn('Feature: ', SpecContext.Feature.Title);
// Inside a scenario Rule is always assigned: one declared without an explicit Rule belongs
// to an implicit Rule, whose Description is empty. It is nil only outside a scenario β in a
// suite or feature hook.
if SpecContext.Rule.Description <> '' then
WriteLn('Rule: ', SpecContext.Rule.Description);
// Current step's DataTable (nil if none)
if Assigned(SpecContext.DataTable) then
WriteLn('Rows: ', SpecContext.DataTable.RowCount);
end)ISpecContext provides
| Property | Description |
|---|---|
Suite |
The containing Suite |
Feature |
The containing Feature |
Rule |
The containing Rule. Always assigned inside a scenario β implicit, with an empty Description, when none was declared; nil outside a scenario |
Scenario |
The current scenario (or Example) |
Step |
The step being executed |
DataTable |
The stepβs data table (nil if none) |
SuiteContext |
The suite context, or nil if none was declared |
FeatureContext |
The feature context, or nil if none was declared |
ScenarioContext |
The World of the running scenario |
Injector |
The injector of this run, for resolving a context by type |
Dependency Injection
Dependency Injection with [MiniInject]
π Language: English | EspaΓ±ol
MiniSpec includes a lightweight dependency injection system for fields and properties marked with [MiniInject].
Basic Usage
Automatic injection of FeatureContext:
uses
Daf.MiniSpec; // MiniInject travels in the facade
type
TFeatureContext = class
public
SharedValue: Integer;
end;
TWorld = class
private
[MiniInject] FCtx: TFeatureContext; // Automatically injected
public
property Ctx: TFeatureContext read FCtx;
end;
Feature('...')
.UseFeatureContext<TFeatureContext> // Registers TFeatureContext in Injector
.UseWorld<TWorld> // When creating World, injects FCtxWhere the mark may go
On a field or on a property. A field is reached whatever its visibility, strict private included; a property is reached only when it is public or published, which is what Delphiβs default RTTI emits for properties. Marking a private property has no effect and raises no error.
TInjectorService also needs the property to be writable. A field always is.
The three contexts, and which one wins
An [MiniInject] property receives any of the three contexts alive while a step runs: the scenarioβs own World, the feature context declared with UseFeatureContext<T>, and the suite context declared with UseSuiteContext<T>.
When more than one of them satisfies the type asked for, the winner is declared:
- An exact type match, looking at scenario, then feature, then suite.
- Failing that, a descendant of the type asked for, in that same order.
So a property declared with a base class two contexts descend from receives the one nearest to the step, and a property declared with the exact type of the suite context receives that one even when a feature context descends from it.
The injector belongs to the run: what it holds are the contexts of the run in progress, and it does not outlive it. A nested run does not see the contexts of the one that launched it.
TInjectorService API
| Method | Description |
|---|---|
Register(Scope, Instance) |
Publishes the context of a scope: csScenario, csFeature, csSuite |
Unregister(Scope) |
Empties a scope |
Resolve(AClass) |
Gets the live context compatible with that class, by the precedence above |
Resolve<T> |
Generic version of Resolve |
InjectInto(Target) |
Injects contexts into [MiniInject] marked fields and properties |
Clear |
Empties the three scopes |
The runner calls Register and Unregister itself as each context is created and destroyed. There is no registry of loose services: a collaborator a scenario needs is built in the World, or in the feature or suite context if it has to be shared.
Injection Errors
If a member marked with [MiniInject] cannot be injected, EInjectionError is raised:
- The member is not a class type
- The property has no setter
- No registered service compatible with type
The message names the member, its type and the class it belongs to.
β Configuration | Next: Reporters β
Test Doubles
MiniSpec Doubles β Test Doubles for Delphi
π Language: English | EspaΓ±ol
A minimalist, fluent API for test doubles that matches MiniSpecβs elegance. Create stubs, mocks, and spies with an intuitive chainable syntax.
Table of Contents
Quick Start
uses
Daf.MiniSpec,
Daf.MiniSpec.Doubles;
type
IUserRepository = interface
function FindById(Id: Integer): IUser;
procedure Save(User: IUser);
end;
initialization
Feature('User Service')
.UseWorld<TUserWorld>
.Scenario('Update user email')
.Given('a user repository', procedure(W: TUserWorld)
begin
W.Repo := Stub<IUserRepository>
.Setup('FindById').WithArgs([42]).Returns(TUser.Create('John'))
.Instance;
end)
.&And('a user service', procedure(W: TUserWorld)
begin
W.Service := TUserService.Create(W.Repo); // building the SUT is setup, not the event
end)
.When('updating the email', procedure(W: TUserWorld)
begin
W.Service.UpdateEmail(42, 'john@new.com');
end)
.&Then('the user is saved', procedure(W: TUserWorld)
begin
Expect(W.Repo.WasCalled('Save')).ToBeTrue;
end)Stubs
Stubs provide canned answers to method calls. Use them when you need to control what collaborators return.
Basic Stub
var Calculator := Stub<ICalculator>
.Setup('Add').Returns(42)
.Instance;
// Calculator.Add(anything) returns 42Conditional Returns with Arguments
var Service := Stub<IUserService>
.Setup('FindById').WithArgs([1]).Returns(User1)
.Setup('FindById').WithArgs([2]).Returns(User2)
.Setup('FindById').Raises(Exception.Create('FindById has no Setup for these arguments')) // Default: make it fail
.Instance;
// Service.FindById(1) β User1
// Service.FindById(2) β User2
// Service.FindById(999) β raises: the scenario never said what to returnβ οΈ A Stub with no matching Setup returns the typeβs zero (False, nil, 0, ''), silently. That zero may be exactly a βgoodβ value and give a false green: the test passes although the scenario never said what the double should return. That is why the example ends with a default Setup that raises an exception instead of .Returns(nil): a double the scenario has not told what to return must fail. Do the same in your stubs whenever the typeβs zero could pass for a valid result, or mark the double strict.
Strict Mode
.Strict makes a Stub or a Mock raise EUnconfiguredCall when a function is called and no Setup matches, instead of returning the typeβs zero. The message names the interface, the method and the arguments:
var Service := Stub<IUserService>.New.Strict
.Setup('FindByName').WithArgs([Arg.NotNull]).Returns(7)
.Instance;
// Service.FindByName('ana') returns 7
// Service.FindByName('') raises EUnconfiguredCall: no Setup matches IUserService.FindByName('')- Any
Setupthat matches counts, whatever it configures (Returns,Raises,Executes). ASetupwithoutWithArgsaccepts every call. - Procedures are not affected: a call to a procedure with no
Setupis recorded and does not raise. - The call that raises is recorded first, so
CallsToandWasCalledcount it. EUnconfiguredCalldescends fromEDoubleError.- Call
.Strictright afterNew, in theGiven. It only affects the calls that come after it.
β οΈ If the code under test catches every exception, it also swallows EUnconfiguredCall and the scenario can still pass. Without .Strict, a double returns the typeβs zero.
Raising Exceptions
var Service := Stub<IPaymentService>
.Setup('Charge').WithArgs([0]).Raises(EInvalidAmount.Create('Amount must be positive'))
.Setup('Charge').Returns(True)
.Instance;
// Service.Charge(0) raises EInvalidAmount
// Service.Charge(100) returns TrueCustom Behavior
var Counter := 0;
var Service := Stub<ICounter>
.Setup('Increment').Executes(
procedure(Args: TArray<TValue>)
begin
Inc(Counter);
end)
.Instance;Checking Calls
var Stub := Stub<ILogger>
.Setup('Log').Returns(nil)
.Instance;
// ... use the stub ...
if Stub.WasCalled('Log') then
WriteLn('Log was called ', Stub.CallsTo('Log'), ' times');
// Get last call details
var LastCall := Stub.LastCallTo('Log');
WriteLn('Last message: ', LastCall.Args[0].AsString);Mocks
Mocks verify interactions. Use them when the behavior youβre testing is about how your code interacts with collaborators.
Basic Expectations
var Logger := Mock<ILogger>
.Expects('Log').Once
.Expects('Flush').AtLeastOnce
.Instance;
// ... use logger in your code ...
Logger.Verify; // Raises EExpectationFailed if expectations not metExpectation Types
.Expects('Method').Never // Must not be called
.Expects('Method').Once // Exactly one call
.Expects('Method').Twice // Exactly two calls
.Expects('Method').AtLeastOnce // One or more calls
.Expects('Method').AtMostOnce // Zero or one call
.Expects('Method').Exactly(5) // Exactly N callsCombined Setup and Expectations
var PaymentGateway := Mock<IPaymentGateway>
// Setup return values
.Setup('Authorize').Returns(True)
.Setup('Capture').Returns('TXN-123')
// Define expectations
.Expects('Authorize').Once
.Expects('Capture').Once
.Expects('Refund').Never
.Instance;
// ... process payment ...
PaymentGateway.Verify;Argument-Specific Expectations
var Notifier := Mock<INotifier>
.Expects('Send').WithArgs(['admin@example.com']).Once
.Expects('Send').WithArgs(['user@example.com']).Once
.Instance;Strict Mocks
Mock<T>.New.Strict behaves like a strict stub. In a mock, Expects does not count as configuration: a function with Expects and no Setup that matches raises EUnconfiguredCall, because Expects says how many times it is called, not what it returns. Add the Setup:
var Calc := Mock<ICalculator>.New.Strict
.Setup('Add').Returns(100)
.Expects('Add').Once
.Instance;Spies
Spies wrap real instances, tracking calls while preserving original behavior.
var RealCalc := TCalculator.Create as ICalculator;
var Spy := SpyOn<ICalculator>(RealCalc)
.TrackAll
.Instance;
// Use spy - calls go to real implementation
var Result := Spy.Add(2, 3); // Returns 5 (real behavior)
// Check what happened
Expect(Spy.CallsTo('Add')).ToEqual(1);
Expect(Spy.LastCallTo('Add').Args[0].AsInteger).ToEqual(2);Selective Tracking
var Spy := SpyOn<IService>(RealService)
.Track('ImportantMethod')
.Track('AnotherMethod')
.Instance;Argument Matchers
For flexible argument matching in stubs and mocks.
Built-in Matchers
uses Daf.MiniSpec.Doubles;
// Match any value
.Setup('Process').WithArgs([Arg.Any]).Returns(True)
// Match non-null/non-empty
.Setup('Save').WithArgs([Arg.NotNull]).Returns(True)
// Match string containing substring
.Setup('Log').WithArgs([Arg.Contains('error')]).Returns(nil)
// Match whatever a predicate accepts
.Setup('Listen').WithArgs([Arg.Matching<Integer>(
function(Port: Integer): Boolean
begin
Result := (Port >= 1) and (Port <= 65535);
end)]).Returns(True)Arg.Matching<T> takes the condition itself. The value reaches the predicate already converted to T, so a range is one of these and not a matcher of its own. An argument of another type does not match, and the predicate is not asked.
Example with Matchers
Setups are resolved from the last declared backwards, and one carrying matchers is preferred to one without. So the narrowest goes last: declaring it first does not protect it from a wider one declared after.
var EmailService := Stub<IEmailService>
.Setup('Send')
.WithArgs([Arg.NotNull, Arg.NotNull])
.Returns(True)
.Setup('Send')
.WithArgs([Arg.Contains('@admin'), Arg.Any])
.Raises(ESecurityException.Create('Cannot email admins'))
.Instance;
// EmailService.Send('admin@co.com', 'Hi') β raises
// EmailService.Send('user@co.com', 'Hi') β True
// EmailService.Send('', 'Hi') β False: nothing matched, so the return type's zero comes back
// (careful: a silent False can be a false green; add a default Setup that raises)Best Practices
1. Prefer Stubs Over Mocks
Use stubs when you only need to provide test data. Reserve mocks for when the interaction itself is what youβre testing.
// β
Good: Stub for data
var Repo := Stub<IRepository>
.Setup('Find').Returns(TestData)
.Instance;
// β
Good: Mock when interaction matters
var Notifier := Mock<INotifier>
.Expects('SendAlert').Once
.Instance;2. Keep Doubles Simple
Donβt over-configure. Set up only what the test needs.
// β Too much setup
var Service := Stub<IService>
.Setup('Method1').Returns(...)
.Setup('Method2').Returns(...)
.Setup('Method3').Returns(...)
.Setup('Method4').Returns(...)
.Instance;
// β
Just what this test needs
var Service := Stub<IService>
.Setup('GetUser').Returns(TestUser)
.Instance;3. One Mock Per Test
Avoid multiple mocks in a single scenario. If you need many, consider if your design needs refactoring.
4. Verify at the End
Call Verify in your Then step, after all interactions have occurred.
.&Then('notifications were sent', procedure(W: TWorld)
begin
W.NotifierMock.Verify;
end)5. Use Descriptive Failures
The verification failures are descriptive:
Mock verification failed:
"SendEmail" expected once, but called 0 times
"Log" expected never, but called 3 times
Integration with MiniSpec
Doubles work seamlessly with MiniSpecβs World pattern:
type
TOrderWorld = class
Repo: IStubSetup<IOrderRepository>;
Notifier: IMockExpect<INotifier>;
Service: TOrderService;
destructor Destroy; override;
end;
destructor TOrderWorld.Destroy;
begin
Service.Free;
inherited;
end;
initialization
Feature('Order Processing')
.UseWorld<TOrderWorld>
.Background
.Given('a stubbed repository', procedure(W: TOrderWorld)
begin
W.Repo := Stub<IOrderRepository>
.Setup('FindById').Returns(TOrder.Create(1, 'Pending'));
end)
.&And('a mock notifier', procedure(W: TOrderWorld)
begin
W.Notifier := Mock<INotifier>
.Setup('Notify').Returns(nil);
end)
.Scenario('Completing an order sends notification')
.Given('the service', procedure(W: TOrderWorld)
begin
W.Notifier.Expects('Notify').Once;
W.Service := TOrderService.Create(W.Repo.Instance, W.Notifier.Instance);
end)
.When('completing order #1', procedure(W: TOrderWorld)
begin
W.Service.Complete(1);
end)
.&Then('customer is notified', procedure(W: TOrderWorld)
begin
W.Notifier.Verify;
end)Requirements
- Delphi 12 Athens or later
- Interfaces with RTTI enabled (
{$M+}or{$TYPEINFO ON})
Part of MiniSpec β BDD Testing Framework for Delphi
Reporters
Reporters
π Language: English | EspaΓ±ol
Syntax: -r <name>:<option1>=<value>,<option2>=<value>,...
| Reporter | Options | Example |
|---|---|---|
console |
(none) | -r console |
json |
output=<file> |
-r json:output=report.json |
junit |
output=<file> |
-r junit:output=results.xml |
gherkin |
output=<dir> |
-r gherkin:output=features/ |
live |
port=<num>, wait=<ms> |
-r live:port=8080,wait=5000 |
Multiple Reporters
You can use multiple reporters in the same run by repeating the -r option:
# Console + JUnit for CI + JSON for analysis
MyApp.exe -r console -r junit:output=results.xml -r json:output=report.jsonAll reporters receive the same events and generate their output simultaneously.
Filtering and feature suppression
When -f is used, reporters only output features that have at least one executed scenario. Features entirely filtered out are silently omitted β from the console, from the JUnit XML, from the JSON, and from the Gherkin files.
JUnit Reporter
Generates XML in JUnit format for CI/CD integration. Compatible with GitHub Actions, GitLab CI, Jenkins, Azure DevOps.
Each scenario that ran, and each row of a Scenario Outline, is a test case. A failed or Undefined one carries a <failure>, and a Pending one a <skipped>, whose message names the step that left it so. A scenario that did not run, such as one the filter left out, is not listed.
Each hook that raised, of the suite or of a feature, is a test case with a <failure> that carries the description of the hook and its message. Those of a feature go in its block, and those of the suite in a block named after the suite. The attributes count the cases listed: failures takes the failed and Undefined scenarios and the hooks, and skipped the Pending ones. --allow-undefined and --strict-pending change the exit code, not the report. Cucumber, since 6.11, reports a Pending scenario as a failure; MiniSpec keeps it as skipped, as its exit code does.
JSON Reporter
Each scenario, each row of a Scenario Outline and each Background carries a status: pass, fail, pending, undefined, or skip when it did not run. Each step carries a status with the names of Cucumberβs JSON formatter, passed, failed, skipped, pending and undefined, and success, true only for a step that passed. The root carries the totals: passCount, failCount, skipCount, pendingCount and undefinedCount.
Live Reporter
By default waits 3 seconds for browser connection. Use wait=0 to disable.
Configuration File
MiniSpec creates MiniSpec.ini in the executable directory if it doesnβt exist:
[minispec]
reporters=console,live
filter=@unit
pause=true
[reporter.live]
port=8080
wait=3000
[reporter.junit]
output=results.xmlThe reporters key accepts multiple reporter names separated by commas. Each reporter can have its own [reporter.<name>] section for options.
Reporters declared in [reporter.X] sections are automatically registered even if not listed in the reporters key. For example, the following is equivalent to reporters=console,live:
[minispec]
reporters=console
[reporter.live]
port=9000Backward compatibility: the old
reporter=(singular) key is still read.
Command line options take priority over the file.
β Dependency Injection | Next: Command Line β
Command Line
Command Line
π Language: English | EspaΓ±ol
| Option | Description |
|---|---|
-h, --help |
Show help |
-f, --filter <expr> |
Filter scenarios (see Tags and Filtering) |
-t, --tags |
List all tags with counts |
-q, --query <expr> |
Show matching scenarios (without executing) |
-r, --reporter <spec> |
Reporter with options (see Reporters) |
--pause |
Wait for key at end |
--dry-run |
List scenarios without executing |
--stacktrace |
Show full stack trace on errors |
--strict-pending |
Treat pending specs as failures (exit code 1) |
--allow-undefined |
Undefined steps do not set the exit code (still counted and reported) |
π‘
--stacktracerequires a stack trace library (JclDebug, MadExcept, EurekaLog).
What sets the exit code
A failed spec and an undefined step set it to 1; pending specs do not, unless you ask with --strict-pending. An undefined step is one whose description matched no binding β an unintentional omission, not work in progress β so it fails by default: otherwise renaming a step definition would leave the suite green while that step never runs.
--allow-undefined is for a suite that marks a step undefined on purpose, which is the only way to check that the steps after it are skipped. It releases the exit code and nothing else: the summary line still counts the undefined steps and the reporter still marks them. A suite that needs it can declare it itself with MiniSpec.AllowUndefined(True), instead of relying on whoever launches it to pass the flag.
Examples
# Help
MyApp.exe -h
# Run only unit tests
MyApp.exe -f "@unit"
# See which scenarios match without executing
MyApp.exe -q "@integration"
# List all tags
MyApp.exe -t
# Run with JUnit output for CI
MyApp.exe -r junit:output=results.xml
# Real-time dashboard
MyApp.exe -r live:port=8080
# Multiple reporters simultaneously
MyApp.exe -r console -r junit:output=results.xml -r json:output=report.jsonConfiguration File
If a MiniSpec.ini file does not exist in the executable directory, MiniSpec creates one with default settings on first run. On subsequent runs, options are loaded from this file.
Command line options always take priority over the file. See Reporters for full INI syntax.
[minispec]
reporters=console,live
filter=@unit
pause=true
[reporter.live]
port=8080β Reporters | Next: Nested Runs β
Nested Runs
Nested Runs
π Language: English | EspaΓ±ol
A TSpecRun is a self-contained execution: it owns its suite, its context, its injector, its options and its runner with its listeners. It can be launched inside a running scenario without altering the counts of the run that contains it and without writing a line into its report.
That is what makes the rules of the runner checkable with specs instead of by hand.
Building and running one
uses Daf.MiniSpec, Daf.MiniSpec.Types, Daf.MiniSpec.Runner;
type
TToyWorld = class
public
Touched: Boolean;
end;
var Run := TSpecRun.Create('Nested');
try
Feature('A toy feature that fails', Run.Suite)
.UseWorld<TToyWorld>
.Scenario('It fails on purpose')
.&Then('one is not two', procedure(W: TToyWorld)
begin
Expect(1).ToEqual(2);
end);
Run.Execute;
Expect(Run.FailCount).ToEqual(1); // the nested run counted its own failure
Expect(Run.ExitCode).ToEqual(1); // and says which exit code its counts deserve
Expect(ExitCode).ToEqual(0); // the process is untouched
finally
Run.Free;
end;TSpecRun is a plain class, so it is freed by whoever created it β usually the World of the scenario that runs it.
Feature(Description, Suite) is the overload that declares the feature in the suite it is given instead of in the suite of the process. It is the only way a feature tree reaches a nested run: the one-argument Feature always declares into the process suite.
What stays outside a nested run
The split with MiniSpec follows one rule: whatever only makes sense once per process stays in the facade β the command-line arguments, the .ini file, the banner and the exit code of the process.
Run.ExitCode is the code that execution deserves, as a function of its own counts. Whoever launched it decides what to do with it; only the root run carries it to the process.
Reports
A nested run prints nothing until it is given a destination:
Run.AddListener(Listener);With no listener there is no output, which is what allows a run to be nested inside a scenario without dirtying its report. The available destinations are in Reporters.
Its own suite context
Run.UseSuiteContext<TMyContext>;Fixes how the suite context of this execution is built. An exception escaping that constructor is the one case that escapes Run.Execute: whatever blows up inside a scenario is caught by the runner itself and counted as a failure.
What a nested run reports
| Member | Answers |
|---|---|
Suite |
The suite of this execution β what Feature(Description, Suite) takes |
Runner |
Its runner, with its own listeners |
Options |
The options of this execution, independent of the process ones |
Injector |
Its injector |
PassCount FailCount SkipCount PendingCount UndefinedCount TotalCount |
Its own counts |
FeatureCount |
Features declared in its suite |
ElapsedMs |
What this execution took |
ExitCode |
The code its counts deserve |
β Command Line | Back to Guide β
Testing Patterns
Testing Patterns with MiniSpec
π Language: English | EspaΓ±ol
This guide complements the User Guide by showing how to adapt BDD to different testing levels. Although BDD was born to capture business requirements, its expressive vocabulary is useful at any level of the test pyramid.
β οΈ The file names in this guide are a suggestion for your project, not DAFβs own convention. Here the suffix carries the test level: .Spec.pas for a class, .Feat.pas for a feature. DAFβs own tree names every spec [Feature].Feat.pas and every step file [Feature].Steps.pas, whatever the level.
Table of Contents
- The Test Pyramid
- E2E Tests: Specifying the System
- Unit Tests: Specifying Classes
- Integration Tests: Specifying APIs
- Organizing by Test Type
- Conclusion
The Test Pyramid
/\
/ \ E2E Tests
/ \ (Complete system, business requirements)
/------\
/ \ Integration Tests
/ \ (APIs, services, connected components)
/------------\
/ \ Unit Tests
/________________\(Individual classes, pure functions)
| Level | SUT | Speed | Fragility | Focus |
|---|---|---|---|---|
| E2E | Complete system | Slow | High | Business requirements |
| Integration | API / Service | Medium | Medium | Contracts between components |
| Unit | Class / Function | Fast | Low | Isolated behavior |
MiniSpec can be used at all three levels. The key is what you describe and how you structure your specifications.
Each level is the acceptance criterion of a contract with its consumer: the product, with whoever uses it (business language); an API, with other systems (technical language); a class, with the developers who use it (its public API). The pyramid is a cost criterion (speed, fragility), not a statement about which level βisβ the acceptance criterion.
E2E is not one more level, but the fidelity at which specs are run: the same specs, against real infrastructure instead of doubles. It goes in a separate suite, with its cost in plain sight. In this guide βE2Eβ names the product level, where that cost is usually felt.
E2E Tests: Specifying the System
This is the βclassicβ use of BDD, covered in detail in the User Guide. The SUT is the complete system and you describe business requirements:
Feature('''
Checkout Process
As a customer
I need to complete my purchase
To receive products at my address
@e2e @checkout
''')
.UseWorld<TCheckoutWorld>
.Scenario('Successful purchase with credit card')
.Given('I have products in the cart', procedure(W: TCheckoutWorld)
begin
W.Cart.Add(TProduct.Create('Laptop', 999.99));
end)
.When('I complete payment with valid card', procedure(W: TCheckoutWorld)
begin
W.Checkout.Pay(TCreditCard.Create('4111111111111111'));
end)
.&Then('I receive order confirmation', procedure(W: TCheckoutWorld)
begin
Expect(W.Checkout.OrderConfirmed).ToBeTrue;
Expect(W.Checkout.OrderNumber).ToMatch('^\d{8}$');
end)E2E test characteristics:
- Business vocabulary, not technical
- The World orchestrates multiple components
- They are the acceptance criterion of the product towards whoever uses it (every level has its own: see the pyramid)
- They can run with doubles or, in a separate E2E suite, against real infrastructure (database, external services); that fidelity is what makes them slow, not the level
Unit Tests: Specifying Classes
When the SUT is an individual class, BDD remains useful. Think of frameworks like RSpec (Ruby) or Jest (JavaScript) that use describe() to group behaviors.
The Pattern: Feature = Class, Rule = Method/Responsibility, Scenario = Example
unit TStringBuilder.Spec;
interface
implementation
uses
System.SysUtils,
Daf.MiniSpec;
type
TStringBuilderWorld = class
public
SUT: TStringBuilder; // System Under Test
Result: string;
destructor Destroy; override;
end;
destructor TStringBuilderWorld.Destroy;
begin
SUT.Free;
inherited;
end;
initialization
Feature('''
TStringBuilder @unit
Class for building strings efficiently
through incremental concatenation.
''')
.UseWorld<TStringBuilderWorld>
.Background
.Given('an empty StringBuilder', procedure(W: TStringBuilderWorld)
begin
W.SUT := TStringBuilder.Create;
end)
.Rule('Append: adds text at the end')
.Scenario('Append a string')
.When('I add "Hello"', procedure(W: TStringBuilderWorld)
begin
W.SUT.Append('Hello');
end)
.&Then('content is "Hello"', procedure(W: TStringBuilderWorld)
begin
Expect(W.SUT.ToString).ToEqual('Hello');
end)
.Scenario('Chained Append')
.When('I add "Hello" then " World"', procedure(W: TStringBuilderWorld)
begin
W.SUT.Append('Hello').Append(' World');
end)
.&Then('content is "Hello World"', procedure(W: TStringBuilderWorld)
begin
Expect(W.SUT.ToString).ToEqual('Hello World');
end)
.Rule('Clear: empties the content')
.Scenario('Clear after adding')
.Given('existing content', procedure(W: TStringBuilderWorld)
begin
W.SUT.Append('Existing content');
end)
.When('I call Clear', procedure(W: TStringBuilderWorld)
begin
W.SUT.Clear;
end)
.&Then('content is empty', procedure(W: TStringBuilderWorld)
begin
Expect(W.SUT.ToString).ToEqual('');
Expect(W.SUT.Length).ToEqual(0);
end)
.Rule('Length: returns current length')
.Scenario('Initial Length is zero')
.When('I construct a StringBuilder', procedure(W: TStringBuilderWorld)
begin
W.SUT.Free; // The event is the construction, replacing the Background's SUT
W.SUT := TStringBuilder.Create;
end)
.&Then('Length is 0', procedure(W: TStringBuilderWorld)
begin
Expect(W.SUT.Length).ToEqual(0);
end)
.ScenarioOutline('Length after Append')
.When('I add <text>', procedure(W: TStringBuilderWorld)
begin
W.SUT.Append(W.Text);
end)
.&Then('Length is <length>', procedure(W: TStringBuilderWorld)
begin
Expect(W.SUT.Length).ToEqual(W.Length);
end)
.Examples([
['text', 'length'],
['', 0],
['a', 1],
['Hello', 5],
['γγγ«γ‘γ―', 5] // Unicode
])
end.Conventions for Unit Tests
| Element | Convention | Example |
|---|---|---|
| Feature | Class name | TStringBuilder, TCalculator |
| Rule | Method or behavior group | Append: adds text, Input validation |
| Scenario | Specific behavior case | Append empty string |
| Tag | @unit for filtering |
TStringBuilder @unit |
| World | Contains only SUT and test data | TStringBuilderWorld |
Comparison with RSpec
If you come from RSpec, hereβs the equivalence:
# RSpec (Ruby)
describe TStringBuilder do
describe '#append' do
it 'adds text to the end' do
builder = TStringBuilder.new
builder.append('Hello')
expect(builder.to_s).to eq('Hello')
end
end
end// MiniSpec (Delphi)
Feature('TStringBuilder @unit')
.UseWorld<TWorld>
.Rule('Append: adds text at the end')
.Scenario('Append a string')
.Given('an empty StringBuilder', ...)
.When('I add "Hello"', ...)
.&Then('content is "Hello"', ...)The structure is similar: - describe Class β Feature('Class') - describe '#method' β .Rule('Method: description') - it 'behavior' β .Scenario('behavior')
Integration Tests: Specifying APIs
When the SUT is an API (REST, GraphQL, gRPCβ¦), each endpoint or operation can be a Feature or Rule.
The Pattern: Feature = Resource, Rule = Obligation of an endpoint, Scenario = Example
unit API.Users.Spec;
interface
implementation
uses
System.SysUtils,
System.JSON,
System.Net.HttpClient,
Daf.MiniSpec;
type
TApiWorld = class
public
Client: THTTPClient;
Response: IHTTPResponse;
ResponseJson: TJSONObject;
UserId: string;
destructor Destroy; override;
end;
destructor TApiWorld.Destroy;
begin
ResponseJson.Free;
Client.Free;
inherited;
end;
initialization
Feature('''
API: /users @integration @api
User management via REST API.
Base URL: http://localhost:3000/api/v1
''')
.UseWorld<TApiWorld>
.Background
.Given('a configured HTTP client', procedure(W: TApiWorld)
begin
W.Client := THTTPClient.Create;
W.Client.ContentType := 'application/json';
end)
.&And('a server free of data from earlier scenarios', procedure(W: TApiWorld)
begin
TestServer.CleanupTestData; // every scenario starts clean: no order dependence
end)
.Rule('GET /users - List users')
.Scenario('Empty list when no users exist')
.Given('a server with no users', procedure(W: TApiWorld)
begin
TestServer.CleanupTestData;
end)
.When('GET /users', procedure(W: TApiWorld)
begin
W.Response := W.Client.Get('http://localhost:3000/api/v1/users');
end)
.&Then('responds 200 OK', procedure(W: TApiWorld)
begin
Expect(W.Response.StatusCode).ToEqual(200);
end)
.&And('returns empty array', procedure(W: TApiWorld)
begin
var Json := TJSONObject.ParseJSONValue(W.Response.ContentAsString);
Expect(Json is TJSONArray).ToBeTrue;
Expect((Json as TJSONArray).Count).ToEqual(0);
Json.Free;
end)
.Rule('POST /users - Create user')
.Scenario('Create user with valid data')
.When('POST /users with name and email', procedure(W: TApiWorld)
begin
var Body := TJSONObject.Create;
Body.AddPair('name', 'John Doe');
Body.AddPair('email', 'john@example.com');
W.Response := W.Client.Post(
'http://localhost:3000/api/v1/users',
TStringStream.Create(Body.ToString)
);
Body.Free;
end)
.&Then('responds 201 Created', procedure(W: TApiWorld)
begin
Expect(W.Response.StatusCode).ToEqual(201);
end)
.&And('returns user with ID', procedure(W: TApiWorld)
begin
W.ResponseJson := TJSONObject.ParseJSONValue(
W.Response.ContentAsString) as TJSONObject;
Expect(W.ResponseJson.GetValue('id')).ToNotBeNull;
Expect(W.ResponseJson.GetValue<string>('name')).ToEqual('John Doe');
W.UserId := W.ResponseJson.GetValue<string>('id');
end)
.Scenario('Error with invalid email')
.When('POST /users with malformed email', procedure(W: TApiWorld)
begin
var Body := TJSONObject.Create;
Body.AddPair('name', 'John');
Body.AddPair('email', 'not-an-email');
W.Response := W.Client.Post(
'http://localhost:3000/api/v1/users',
TStringStream.Create(Body.ToString)
);
Body.Free;
end)
.&Then('responds 400 Bad Request', procedure(W: TApiWorld)
begin
Expect(W.Response.StatusCode).ToEqual(400);
end)
.&And('includes error message', procedure(W: TApiWorld)
begin
W.ResponseJson := TJSONObject.ParseJSONValue(
W.Response.ContentAsString) as TJSONObject;
Expect(W.ResponseJson.GetValue<string>('error')).ToContain('email');
end)
.Rule('GET /users/{id} - Fetch a user')
.Scenario('User exists')
.Given('an existing user', procedure(W: TApiWorld)
begin
// Create the user first
var Body := TJSONObject.Create;
Body.AddPair('name', 'Jane');
Body.AddPair('email', 'jane@example.com');
var Resp := W.Client.Post(
'http://localhost:3000/api/v1/users',
TStringStream.Create(Body.ToString)
);
var Json := TJSONObject.ParseJSONValue(Resp.ContentAsString) as TJSONObject;
W.UserId := Json.GetValue<string>('id');
Json.Free;
Body.Free;
end)
.When('GET /users/{id}', procedure(W: TApiWorld)
begin
W.Response := W.Client.Get(
'http://localhost:3000/api/v1/users/' + W.UserId);
end)
.&Then('responds 200 OK', procedure(W: TApiWorld)
begin
Expect(W.Response.StatusCode).ToEqual(200);
end)
.&And('returns the user data', procedure(W: TApiWorld)
begin
W.ResponseJson := TJSONObject.ParseJSONValue(
W.Response.ContentAsString) as TJSONObject;
Expect(W.ResponseJson.GetValue<string>('name')).ToEqual('Jane');
end)
.Scenario('User does not exist')
.Given('user 999999 does not exist', procedure(W: TApiWorld)
begin
// The Background left the server with no users; we assert it
Expect(W.Client.Get('http://localhost:3000/api/v1/users/999999').StatusCode).ToEqual(404);
end)
.When('GET /users/999999', procedure(W: TApiWorld)
begin
W.Response := W.Client.Get(
'http://localhost:3000/api/v1/users/999999');
end)
.&Then('responds 404 Not Found', procedure(W: TApiWorld)
begin
Expect(W.Response.StatusCode).ToEqual(404);
end)
end.Conventions for Integration Tests
| Element | Convention | Example |
|---|---|---|
| Feature | Endpoint or resource | API: /users, GraphQL: User |
| Rule | HTTP Method + Route | GET /users, POST /users |
| Scenario | Specific use case | Create user with valid data |
| Tag | @integration or @api |
API: /users @integration @api |
Common HTTP Status Codes
To assert on HTTP responses:
// Reusable helpers
procedure ExpectSuccess(Response: IHTTPResponse);
begin
Expect(Response.StatusCode).ToBeBetween(200, 299);
end;
procedure ExpectClientError(Response: IHTTPResponse);
begin
Expect(Response.StatusCode).ToBeBetween(400, 499);
end;
procedure ExpectServerError(Response: IHTTPResponse);
begin
Expect(Response.StatusCode).ToBeBetween(500, 599);
end;Organizing by Test Type
When you have unit, integration, and E2E tests in the same project, you need a strategy to organize and run them.
Option B: One Executable per Level
Each level gets its own .dpr. Worth it when the levels have very different dependencies, or for CI/CD pipelines that run them in parallel.
MyProject/
βββ src/ # Production code
βββ specs/
β βββ Specs.groupproj # Groups every spec project
β β
β βββ unit/
β β βββ UnitSpecs.dpr # Executable: unit tests only
β β βββ UnitSpecs.dproj
β β βββ TCalculator.Spec.pas
β β βββ TStringBuilder.Spec.pas
β β βββ TValidator.Spec.pas
β β
β βββ integration/
β β βββ IntegrationSpecs.dpr # Executable: integration only
β β βββ IntegrationSpecs.dproj
β β βββ API.Users.Spec.pas
β β βββ API.Orders.Spec.pas
β β βββ Database.Spec.pas
β β
β βββ e2e/
β βββ E2ESpecs.dpr # Executable: E2E only
β βββ E2ESpecs.dproj
β βββ Checkout.Feat.pas
β βββ Registration.Feat.pas
β βββ Search.Feat.pas
Each executable configures its own level:
// UnitSpecs.dpr - fast, no external dependencies
program UnitSpecs;
{$APPTYPE CONSOLE}
uses
Daf.MiniSpec,
TCalculator.Spec in 'TCalculator.Spec.pas',
TStringBuilder.Spec in 'TStringBuilder.Spec.pas',
TValidator.Spec in 'TValidator.Spec.pas';
begin
MiniSpec
.Category('Unit Tests')
.Run;
end.
// IntegrationSpecs.dpr - starts a local TestServer in the Before
program IntegrationSpecs;
{$APPTYPE CONSOLE}
uses
Daf.MiniSpec,
TestServer in '..\common\TestServer.pas',
API.Users.Spec in 'API.Users.Spec.pas',
API.Orders.Spec in 'API.Orders.Spec.pas';
begin
MiniSpec
.Category('Integration Tests')
.Before('Start test server', procedure
begin
TestServer.Start;
end)
.After('Stop test server', procedure
begin
TestServer.Stop;
end)
.Run;
end.
// E2ESpecs.dpr - the whole system
program E2ESpecs;
{$APPTYPE CONSOLE}
uses
Daf.MiniSpec,
Checkout.Feat in 'Checkout.Feat.pas',
Registration.Feat in 'Registration.Feat.pas';
begin
MiniSpec
.Category('E2E Tests')
.Before('Initialize full system', procedure
begin
TestEnvironment.Setup;
end)
.After('Cleanup', procedure
begin
TestEnvironment.Teardown;
end)
.Run;
end.Advantages: small executables, separated dependencies, easy to parallelise in CI. Disadvantages: more projects to maintain.
Summary: When to Use Each Level
| Question | Unit | Integration | E2E |
|---|---|---|---|
| Is the SUT an isolated class? | β | ||
| Are you testing an API or a service? | β | ||
| Are you describing business requirements? | β | ||
| Do you need real infrastructure (DB, services)? | Only in the E2E suite | Only in the E2E suite | |
| Must the test be very fast? | β | ||
| Will non-technical stakeholders read it? | β |
Conclusion
BDD is not exclusive to E2E tests. MiniSpecβs vocabulary β Feature, Rule, Scenario, Given/When/Then β gives a clear structure at any testing level:
- Unit tests: Feature = class, Rule = method or responsibility
- Integration tests: Feature = resource, Rule = the obligation of an endpoint
- Product (E2E): Feature = capability, Rule = business rule
At all three, the Rule is the acceptance criterion of the contract with its consumer and the Scenario is an example of it.
What changes is the vocabulary and the granularity, never the underlying principle: specify behaviour with concrete examples.
For the complete guide, please refer to the Spanish version while we complete the English translation.