Table of Contents

File-Based DSL

TinyBDD.Extensions.FileBased enables writing BDD scenarios in external files (Gherkin .feature files or YAML) that are executed through convention-based driver methods.

Overview

What it does: Allows non-developers to author test specifications in standard Gherkin syntax or YAML format, which are then executed by driver methods in your test code

When to use it:

  • Business analysts or QA engineers need to author test scenarios
  • Test scenarios need to be separated from implementation details
  • Teams want to leverage existing Gherkin knowledge and tooling
  • Large test suites benefit from reducing code duplication

Prerequisites:

  • TinyBDD 3.0.0 or later
  • A test framework adapter (xUnit, NUnit, or MSTest)
  • .NET 8.0 or later

Quick Start

Installation

dotnet add package TinyBDD.Extensions.FileBased
dotnet add package TinyBDD.Xunit  # or TinyBDD.NUnit, TinyBDD.MSTest

1. Create a Gherkin Feature File

Create Features/Calculator.feature:

Feature: Calculator Operations
  Testing basic arithmetic operations

@calculator @smoke
Scenario: Add two numbers
  Given a calculator
  When I add 5 and 3
  Then the result should be 8

2. Implement a Driver

Create a driver class implementing IApplicationDriver:

using TinyBDD.Extensions.FileBased.Core;

public class CalculatorDriver : IApplicationDriver
{
    private int _result;

    [DriverMethod("a calculator")]
    public Task Initialize()
    {
        _result = 0;
        return Task.CompletedTask;
    }

    [DriverMethod("I add {a} and {b}")]
    public Task Add(int a, int b)
    {
        _result = a + b;
        return Task.CompletedTask;
    }

    [DriverMethod("the result should be {expected}")]
    public Task<bool> VerifyResult(int expected)
    {
        return Task.FromResult(_result == expected);
    }

    // Required lifecycle methods
    public Task InitializeAsync(CancellationToken ct = default) => Task.CompletedTask;
    public Task CleanupAsync(CancellationToken ct = default) => Task.CompletedTask;
}

3. Create Test Class

using TinyBDD.Extensions.FileBased;

public class CalculatorTests : FileBasedTestBase<CalculatorDriver>
{
    [Fact]
    public async Task ExecuteCalculatorScenarios()
    {
        await ExecuteScenariosAsync(options =>
        {
            options.AddFeatureFiles("Features/**/*.feature")
                   .WithBaseDirectory(Directory.GetCurrentDirectory());
        });
    }
}

4. Run Tests

dotnet test

Output shows Gherkin-formatted results:

Feature: Calculator Operations
Scenario: Add two numbers
  Given a calculator [OK] 0 ms
  When I add 5 and 3 [OK] 0 ms
  Then the result should be 8 [OK] 0 ms

Core Concepts

Gherkin vs YAML

The extension supports two file formats:

Format Use Case Strengths
Gherkin (.feature) Business-readable specifications Standard syntax, wide tool support, natural language
YAML (.yml) Programmatic test generation Structured data, tooling integration, automation

Recommendation: Use Gherkin for human-authored scenarios. Use YAML when scenarios are generated by tools or need structured parameter passing.

Driver Methods

Driver methods are the bridge between file-based scenarios and test implementation. They use the [DriverMethod] attribute with pattern matching:

[DriverMethod("I add {a} and {b}")]
public Task Add(int a, int b)
{
    _result = a + b;
    return Task.CompletedTask;
}

Pattern matching rules:

  • Patterns are case-insensitive
  • {placeholders} extract parameters from step text
  • Parameters are automatically type-converted
  • Methods can be sync (Task) or return values (Task<bool>, Task<T>)

Step Resolution

Steps from files are matched to driver methods through pattern matching:

  1. Extract pattern: "I add {a} and {b}" becomes regex "I add (.*) and (.*)"
  2. Match step text: "I add 5 and 3" matches with captures ["5", "3"]
  3. Type conversion: Captured strings converted to method parameter types
  4. Invocation: Method called with converted parameters

Boolean Assertions

For Then steps, driver methods can return Task<bool>:

  • true = assertion passes
  • false = assertion fails (throws exception with step description)
[DriverMethod("the result should be {expected}")]
public Task<bool> VerifyResult(int expected)
{
    return Task.FromResult(_result == expected);
}

This pattern is cleaner than using assertion libraries directly in driver methods.

Gherkin Syntax

Standard Gherkin keywords are fully supported:

Feature

Feature: Feature Name
  Optional feature description
  that can span multiple lines

Scenarios

@tag1 @tag2
Scenario: Scenario Name
  Given precondition step
  When action step
  Then assertion step

Step Keywords

  • Given: Setup and preconditions
  • When: Actions being tested
  • Then: Expectations and assertions
  • And: Continues previous keyword
  • But: Continues previous keyword (with contrast)

Scenario Outlines

Parameterized scenarios with example tables:

Scenario Outline: Multiply numbers
  Given a calculator
  When I multiply <a> and <b>
  Then the result should be <expected>

Examples:
  | a | b | expected |
  | 2 | 3 | 6        |
  | 4 | 5 | 20       |
  | 0 | 9 | 0        |

This generates 3 separate test scenarios, one for each example row.

Tags

Tags can be applied to scenarios for organization and filtering:

@smoke @calculator
Scenario: Add two numbers
  ...

Tags are accessible in driver methods through scenario context.

YAML Format

YAML provides an alternative format for programmatic scenario generation:

feature: Calculator Operations
description: Testing basic arithmetic
scenarios:
  - name: Add two numbers
    tags:
      - calculator
      - smoke
    steps:
      - keyword: Given
        text: a calculator
      - keyword: When
        text: I add 5 and 3
        parameters:
          a: 5
          b: 3
      - keyword: Then
        text: the result should be 8
        parameters:
          expected: 8

YAML advantages:

  • Structured parameter passing via parameters section
  • Easier for tool generation
  • Supports complex parameter values (whitespace, special characters)

Usage:

options.AddYamlFiles("Features/**/*.yml")

Configuration

File Discovery

Use glob patterns to discover feature files:

await ExecuteScenariosAsync(options =>
{
    // Single file
    options.AddFeatureFiles("Features/Calculator.feature")

    // All files in directory
    options.AddFeatureFiles("Features/*.feature")

    // Recursive search
    options.AddFeatureFiles("Features/**/*.feature")

    // Multiple patterns
    options.AddFeatureFiles("Features/Smoke/**/*.feature")
           .AddFeatureFiles("Features/Regression/**/*.feature")

    // Base directory for relative paths
    options.WithBaseDirectory(Directory.GetCurrentDirectory());
});

Custom Driver Instantiation

Override CreateDriver() for dependency injection:

public class MyTests : FileBasedTestBase<MyDriver>
{
    private readonly IServiceProvider _serviceProvider;

    public MyTests(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    protected override MyDriver CreateDriver()
    {
        return new MyDriver(
            _serviceProvider.GetRequiredService<IMyService>(),
            _serviceProvider.GetRequiredService<ILogger<MyDriver>>());
    }
}

Driver Lifecycle

Drivers implement IApplicationDriver with lifecycle hooks:

public interface IApplicationDriver
{
    Task InitializeAsync(CancellationToken cancellationToken = default);
    Task CleanupAsync(CancellationToken cancellationToken = default);
}

Lifecycle order:

  1. InitializeAsync() - Called before first scenario
  2. Scenario steps executed via [DriverMethod] methods
  3. CleanupAsync() - Called after all scenarios complete

Advanced Usage

Parameter Extraction Priority

Parameters can come from multiple sources with this priority:

  1. YAML parameters: Explicitly defined in YAML (highest priority)
  2. Pattern placeholders: Extracted from step text
  3. Method defaults: Default parameter values in method signature
[DriverMethod("I process {value}")]
public Task Process(int value, string mode = "standard")
{
    // 'value' from step text
    // 'mode' uses default unless specified in YAML parameters
    return Task.CompletedTask;
}

Complex Parameters

For parameters containing whitespace or special characters, use YAML parameters section:

steps:
  - keyword: When
    text: I login with credentials
    parameters:
      username: "john.doe@example.com"
      password: "P@ssw0rd with spaces!"

Pattern placeholders only capture non-whitespace tokens.

Multiple Scenarios per File

Files can contain multiple scenarios - all are executed sequentially:

Feature: User Management

Scenario: Register user
  Given the registration page
  When I submit valid details
  Then account should be created

Scenario: Login user
  Given a registered user
  When I provide correct credentials
  Then I should be authenticated

Shared Driver State

Driver instances maintain state across steps within a single scenario but are fresh for each scenario:

public class UserManagementDriver : IApplicationDriver
{
    private string? _userId;  // State shared within scenario

    [DriverMethod("I register user {username}")]
    public async Task RegisterUser(string username)
    {
        _userId = await _userService.CreateUserAsync(username);
    }

    [DriverMethod("the user should exist")]
    public Task<bool> UserExists()
    {
        // Can access _userId from previous step
        return Task.FromResult(_userId != null);
    }
}

Best Practices

Write Declarative Steps

# Good: Declarative, describes what not how
Given a user account with premium subscription
When they attempt to download exclusive content
Then download should succeed

# Avoid: Imperative, exposes implementation
Given I call POST /users with {data}
When I call GET /content/123/download
Then response code should be 200

Why: Declarative steps are stable when implementation changes and more readable for non-technical stakeholders.

Keep Drivers Thin

// Good: Delegate to application services
[DriverMethod("I add {a} and {b}")]
public Task Add(int a, int b)
{
    _calculator.Add(a, b);  // Thin wrapper
    return Task.CompletedTask;
}

// Avoid: Business logic in driver
[DriverMethod("I add {a} and {b}")]
public Task Add(int a, int b)
{
    // Don't implement calculator here
    _result = a + b;
    if (a < 0 || b < 0) throw new InvalidOperationException();
    return Task.CompletedTask;
}

Why: Drivers should test the application, not reimplement it. Keep logic in the application layer.

Use Meaningful Pattern Names

// Good: Clear parameter names
[DriverMethod("I login as {username} with password {password}")]

// Avoid: Generic names
[DriverMethod("I login with {arg1} and {arg2}")]

Why: Clear names make step resolution more obvious and improve maintainability.

Prefer Gherkin for Human Authors

Use Gherkin .feature files when scenarios are written by people. Use YAML only when scenarios are generated by tools or need complex parameter structures.

Troubleshooting

Step Not Matching Driver Method

Problem: Step in file doesn't execute, throws "no matching driver method"

Causes:

  1. Pattern doesn't match step text (check case, spacing)
  2. Parameter placeholder mismatch
  3. Driver method not marked with [DriverMethod]

Solution:

// Ensure pattern matches exactly (case-insensitive)
[DriverMethod("I add {a} and {b}")]  // Matches "I add 5 and 3"

// Check method signature
public Task Add(int a, int b)  // Parameters must match placeholders
{
    ...
}

File Not Found

Problem: FileNotFoundException when running tests

Cause: Base directory doesn't point to correct location

Solution:

// Use absolute path or ensure base directory is correct
options.AddFeatureFiles("Features/*.feature")
       .WithBaseDirectory(Path.Combine(
           Directory.GetCurrentDirectory(),
           "..", "..", ".."  // Adjust for your project structure
       ));

// Or use absolute path
options.AddFeatureFiles("/absolute/path/to/Features/*.feature");

Parameter Type Conversion Fails

Problem: Exception during parameter conversion

Cause: Step text contains value that can't convert to parameter type

Solution:

// Ensure step text contains convertible values
// Step: "I add five and three" won't convert to int

// Either use numeric text:
// Step: "I add 5 and 3"

// Or use string parameters and convert in method:
[DriverMethod("I add {a} and {b}")]
public Task Add(string a, string b)
{
    int numA = ParseNumber(a);  // Custom parsing
    int numB = ParseNumber(b);
    _result = numA + numB;
    return Task.CompletedTask;
}

Scenario Outline Not Expanding

Problem: Scenario Outline runs only once instead of once per example

Cause: Empty Examples table or missing Examples section

Solution:

Scenario Outline: Test
  When I multiply <a> and <b>
  Then result is <expected>

Examples:
  | a | b | expected |
  | 2 | 3 | 6        |  # Must have at least one data row

Performance Considerations

File Discovery Overhead

Glob patterns are evaluated at runtime. For large codebases:

// Better: Specific patterns
options.AddFeatureFiles("Features/Smoke/**/*.feature")

// Slower: Very broad patterns
options.AddFeatureFiles("**/*.feature")  // Searches entire project

Driver Instantiation

Drivers are created once per test method. For expensive driver setup:

protected override MyDriver CreateDriver()
{
    // Reuse expensive resources across scenarios
    return new MyDriver(_sharedExpensiveResource);
}

Async Operations

All driver methods are async-friendly. Use async operations for I/O:

[DriverMethod("I fetch data from API")]
public async Task FetchData()
{
    _data = await _httpClient.GetAsync<Data>("/api/data");
}

Return to: Extensions | User Guide