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 [Inject] system
Execution and Output
- Reporters β Console, JSON, JUnit, Gherkin, Live Dashboard
- Command Line β Execution options
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 |
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.
Table of Contents
- The Test Pyramid
- E2E Tests: Specifying the System
- Unit Tests: Specifying Classes
- Integration Tests: Specifying APIs
- Organizing by Test Type
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.
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
- Can be slow (real database, external services)
- Ideal for acceptance criteria
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, Scenario = Method/Behavior
unit TStringBuilder.Spec.pas;
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')
.&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/Endpoint, Scenario = Operation
unit API.Users.Spec.pas;
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)
.Rule('GET /users - List users')
.Scenario('Empty list when no users exist')
.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)
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 |
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: Separate Executables
Separate projects for each type:
specs/
βββ UnitSpecs.dpr # Only @unit
βββ IntegrationSpecs.dpr # Only @integration
βββ E2ESpecs.dpr # Only @e2e
Advantages: - Clear dependency separation - Different Delphi configurations - Smaller executables
Disadvantages: - Multiple projects to maintain - Possible code duplication
For the complete guide, please refer to the Spanish version while we complete the English translation.