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:
- Extract pattern:
"I add {a} and {b}"becomes regex"I add (.*) and (.*)" - Match step text:
"I add 5 and 3"matches with captures["5", "3"] - Type conversion: Captured strings converted to method parameter types
- Invocation: Method called with converted parameters
Boolean Assertions
For Then steps, driver methods can return Task<bool>:
true= assertion passesfalse= 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
parameterssection - 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:
InitializeAsync()- Called before first scenario- Scenario steps executed via
[DriverMethod]methods CleanupAsync()- Called after all scenarios complete
Advanced Usage
Parameter Extraction Priority
Parameters can come from multiple sources with this priority:
- YAML
parameters: Explicitly defined in YAML (highest priority) - Pattern placeholders: Extracted from step text
- 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:
- Pattern doesn't match step text (check case, spacing)
- Parameter placeholder mismatch
- 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");
}
Related Topics
- Gherkin Syntax Reference - Complete Gherkin syntax guide
- YAML Format Reference - YAML schema and examples
- Samples: Calculator Testing - Basic example
- Samples: API Testing - Real-world example
Return to: Extensions | User Guide