Skip to main content

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

Organization and Reuse

Verifications

Vocabulary and Markers

Advanced Configuration

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

← Back to README


Getting Started

Getting Started with MiniSpec

🌍 Language: English | Español

← Back to Guide


Why MiniSpec?

Imagine you could write your application requirements in a way that:

  1. Anyone can read them β€” developers, testers, analysts, clients
  2. They run automatically β€” verifying the code meets the specification
  3. 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?

  1. Feature declares what functionality we’re specifying
  2. UseWorld defines a class containing the SUT and test data
  3. Background prepares common context for all scenarios (creates the calculator)
  4. Scenario describes a concrete use case
  5. When executes the action we want to test (uses the SUT)
  6. 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

← Back to Guide


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

← Back to Guide


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?

  1. The placeholders <A>, <B>, <Result> in descriptions are replaced with concrete values
  2. MiniSpec reads values from the Examples table and injects them into corresponding World properties (that’s why World must have A, B, and Result properties)
  3. 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

← Back to Guide


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

← Back to Guide


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 implementations

Register 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 β†’


Tags and Filtering

Tags and Filtering

🌍 Language: English | Español

← Back to Guide


Tags allow categorizing and filtering scenarios. Add them in the description, preferably at the end:

Feature('''
Calculator

  @math @core
''')

.Scenario('''
  Division by zero
  @error @edge-case
''')

Running with Filters

# Only scenarios with @unit
MyApp.exe -f "@unit"

# Scenarios @unit but NOT @slow
MyApp.exe -f "@unit and ~@slow"

# By feature title
MyApp.exe -f "Feat:Calculator"

# By category
MyApp.exe -f "Cat:Login"

# Complex expressions
MyApp.exe -f "(Feat:Login or @auth) and ~@slow"

Filter Syntax

Expression Meaning
@tag Has the tag
~@tag Does NOT have the tag
Feat:text Feature title contains text
Scen:text Scenario description contains text
Rule:text Rule description contains text
Cat:text Category contains text
and, or Logical operators
() Grouping

⚠️ Tags must start with @. Writing --filter unit will error; use --filter @unit.

⚠️ The text after Feat:, Scen:, Rule: or Cat: is a single word, and there is no quoting. The expression is split on spaces, so -f "Scen:releases one reference" reads Scen:releases and then aborts on the next word with Token inesperado en expresiΓ³n de filtro: one β€” zero specs run, which in a script reads exactly like a suite that passed. Pick one word the description contains.


← Step Bindings | Next: Assertions β†’


Rules

Rules: Grouping by Business Rule

🌍 Language: English | Español

← Back to Guide


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

← Back to Guide


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

← Back to Guide


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 & before Then and And is 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 the Then can only compare what the Given left behind, so the assertion measures the setup rather than the subject.
  • Event in the Then β€” the failure is reported as a failed assertion. With several Then steps, 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

← Back to Guide


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').NoAction

Two questions before accepting it, because most steps that look implicit are not:

  1. Can I trigger it? Then trigger it in the When, and this is not the case.
  2. 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 nothing

The 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

← Back to Guide


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

← Back to Guide


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 programmatically

SpecContext: 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

FeatureContext: Shared Resources Between Scenarios

Unlike the World (which is created fresh for each scenario), the FeatureContext allows sharing resources between all scenarios in a Feature. Useful for expensive-to-create resources (a connection, a read-only cache). It is not for scenario data: if one scenario reads what another left behind, the Feature depends on execution order; each scenario creates its data in its own Given:

type
  TSharedContext = class
  public
    Connection: TDbConnection;  // expensive resource, read-only for the scenarios
  end;

  TScenarioWorld = class
  private
    [MiniInject] FShared: TSharedContext;  // Automatically injected
  public
    LocalData: string;
    Result: string;
    property Shared: TSharedContext read FShared;
  end;

Feature('Database operations')
  .UseFeatureContext<TSharedContext>  // Creates ONE instance for entire Feature
  .UseWorld<TScenarioWorld>    // Each scenario gets its own World
  
  .Scenario('First query')
    .When('query data', procedure(W: TScenarioWorld)
      begin
        // W.Shared points to same TSharedContext: the same connection
        W.Result := W.Shared.Connection.Query('SELECT COUNT(*) FROM users');
      end)
  
  .Scenario('Second query')
    .When('query other data', procedure(W: TScenarioWorld)
      begin
        // Reuses the connection; reads nothing the previous scenario left behind
        W.Result := W.Shared.Connection.Query('SELECT COUNT(*) FROM orders');
      end)

Lifecycle

  • FeatureContext is created at Feature start
  • Destroyed when Feature ends
  • Each ScenarioWorld receives injection of same FeatureContext

← Hooks | Next: Dependency Injection β†’


Dependency Injection

Dependency Injection with [MiniInject]

🌍 Language: English | Español

← Back to Guide


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 FCtx

Where 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:

  1. An exact type match, looking at scenario, then feature, then suite.
  2. 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 42

Conditional 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 Setup that matches counts, whatever it configures (Returns, Raises, Executes). A Setup without WithArgs accepts every call.
  • Procedures are not affected: a call to a procedure with no Setup is recorded and does not raise.
  • The call that raises is recorded first, so CallsTo and WasCalled count it.
  • EUnconfiguredCall descends from EDoubleError.
  • Call .Strict right after New, in the Given. 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 True

Custom 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 met

Expectation 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 calls

Combined 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

← Back to Guide


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.json

All 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.xml

The 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=9000

Backward 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

← Back to Guide


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)

πŸ’‘ --stacktrace requires 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.json

Configuration 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

← Back to Guide


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
      /    \     (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

Using FeatureContext for Shared Resources

In API specs, FeatureContext is for sharing resources (configuration, token, connection), not data: one scenario must not read what another created, or it would depend on order. Each scenario creates its data in its own Given:

type
  TApiContext = class
  public
    BaseUrl: string;
    AuthToken: string;
    CreatedIds: TList<string>;
    constructor Create;
    destructor Destroy; override;
  end;

  TApiWorld = class
  private
    [MiniInject] FCtx: TApiContext;
  public
    Client: THTTPClient;
    Response: IHTTPResponse;
    property Ctx: TApiContext read FCtx;
  end;

Feature('API: /orders @integration')
  .UseFeatureContext<TApiContext>
  .UseWorld<TApiWorld>

  .Before('Start test server', procedure
    begin
      TestServer.Start;
    end)

  .After('Clean up test data', procedure
    begin
      TestServer.CleanupTestData;
      TestServer.Stop;
    end)

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.