Skip to content

Commit cb1fdc9

Browse files
authored
feat(gerkin): Add file-based DSL with Gherkin .feature files and YAML support (#47)
1 parent 443d7f1 commit cb1fdc9

File tree

67 files changed

+8306
-454
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+8306
-454
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,15 @@ jobs:
174174
- name: Install GitVersion
175175
uses: gittools/actions/gitversion/setup@v4
176176
with:
177-
versionSpec: '5.x'
177+
versionSpec: '6.x'
178178

179179
- name: Run GitVersion
180180
id: gitversion
181181
uses: gittools/actions/gitversion/execute@v4
182182

183183
- name: Set version env vars
184184
run: |
185-
echo "PACKAGE_VERSION=${{ steps.gitversion.outputs.nuGetVersionV2 }}" >> $GITHUB_ENV
185+
echo "PACKAGE_VERSION=${{ steps.gitversion.outputs.majorMinorPatch }}" >> $GITHUB_ENV
186186
echo "ASSEMBLY_VERSION=${{ steps.gitversion.outputs.assemblySemVer }}" >> $GITHUB_ENV
187187
echo "FILE_VERSION=${{ steps.gitversion.outputs.assemblySemFileVer }}" >> $GITHUB_ENV
188188

.github/workflows/pr-validation.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
- name: Install GitVersion
4646
uses: gittools/actions/gitversion/setup@v4
4747
with:
48-
versionSpec: '5.x'
48+
versionSpec: '6.x'
4949

5050
- name: Run GitVersion
5151
id: gitversion
@@ -54,7 +54,7 @@ jobs:
5454
- name: Set version
5555
id: version
5656
run: |
57-
VERSION=${{ steps.gitversion.outputs.nuGetVersionV2 }}
57+
VERSION=${{ steps.gitversion.outputs.majorMinorPatch }}
5858
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
5959
echo "📦 Version: $VERSION"
6060

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,7 @@
4040
</PackageVersion>
4141
<PackageVersion Include="xunit.v3" Version="3.2.1" />
4242
<PackageVersion Include="xunit.v3.extensibility.core" Version="3.2.1" />
43+
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
44+
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="10.0.1" />
4345
</ItemGroup>
4446
</Project>

GitVersion.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
mode: MainLine
1+
workflow: GitHubFlow/v1
2+
mode: ContinuousDelivery
23
tag-prefix: 'v'
34
commit-message-incrementing: Enabled
45

README.md

Lines changed: 172 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,31 @@
1616
| **TinyBDD.Xunit.v3** | [![NuGet](https://img.shields.io/nuget/v/TinyBDD.Xunit.v3.svg)](https://www.nuget.org/packages/TinyBDD.Xunit.v3/) | [![NuGet Downloads](https://img.shields.io/nuget/dt/TinyBDD.Xunit.v3.svg)](https://www.nuget.org/packages/TinyBDD.Xunit.v3/) |
1717
| **TinyBDD.NUnit** | [![NuGet](https://img.shields.io/nuget/v/TinyBDD.NUnit.svg)](https://www.nuget.org/packages/TinyBDD.NUnit/) | [![NuGet Downloads](https://img.shields.io/nuget/dt/TinyBDD.NUnit.svg)](https://www.nuget.org/packages/TinyBDD.NUnit/) |
1818
| **TinyBDD.Extensions.DependencyInjection** | [![NuGet](https://img.shields.io/nuget/v/TinyBDD.Extensions.DependencyInjection.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.DependencyInjection/) | [![NuGet Downloads](https://img.shields.io/nuget/dt/TinyBDD.Extensions.DependencyInjection.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.DependencyInjection/) |
19+
| **TinyBDD.Extensions.FileBased** | [![NuGet](https://img.shields.io/nuget/v/TinyBDD.Extensions.FileBased.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.FileBased/) | [![NuGet Downloads](https://img.shields.io/nuget/dt/TinyBDD.Extensions.FileBased.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.FileBased/) |
1920
| **TinyBDD.Extensions.Hosting** | [![NuGet](https://img.shields.io/nuget/v/TinyBDD.Extensions.Hosting.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.Hosting/) | [![NuGet Downloads](https://img.shields.io/nuget/dt/TinyBDD.Extensions.Hosting.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.Hosting/) |
2021
| **TinyBDD.Extensions.Reporting** | [![NuGet](https://img.shields.io/nuget/v/TinyBDD.Extensions.Reporting.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.Reporting/) | [![NuGet Downloads](https://img.shields.io/nuget/dt/TinyBDD.Extensions.Reporting.svg)](https://www.nuget.org/packages/TinyBDD.Extensions.Reporting/) |
2122

2223
---
2324

24-
**TinyBDD** is a minimal, fluent **Behavior-Driven Development** library for .NET.
25-
It provides a lightweight `Given` / `When` / `Then` syntax with optional `And` / `But` chaining, supporting both **sync** and **async** steps.
25+
**TinyBDD** is a minimal, fluent **Behavior-Driven Development** library for .NET.
26+
It supports two complementary approaches:
27+
- **Code-first**: Fluent `Given` / `When` / `Then` syntax directly in C#
28+
- **File-based**: Gherkin `.feature` files and YAML scenarios with convention-based drivers
2629

27-
It is designed to:
30+
Both approaches are designed to:
2831

2932
- Be **framework-agnostic** (works with MSTest, xUnit, NUnit, etc.).
30-
- Keep scenarios **clear and concise** without heavy DSLs or external tooling.
31-
- Support **async and sync predicates** for maximum flexibility.
32-
- Integrate with existing test runners output for easy step visibility.
33+
- Keep scenarios **clear and concise** with readable syntax.
34+
- Support **async and sync** operations for maximum flexibility.
35+
- Integrate with existing test runners' output for easy step visibility.
3336

3437

3538
---
3639

3740
## Features
3841

42+
### Code-First Approach
43+
3944
- **Readable BDD syntax**:
4045
```csharp
4146
await Given("a number", () => 5)
@@ -61,6 +66,33 @@ It is designed to:
6166
But != 11 [OK]
6267
```
6368

69+
### File-Based Approach
70+
71+
- **Gherkin .feature files**:
72+
```gherkin
73+
Feature: Calculator Operations
74+
75+
Scenario: Add two numbers
76+
Given a calculator
77+
When I add 5 and 3
78+
Then the result should be 8
79+
```
80+
81+
- **Convention-based driver methods**:
82+
```csharp
83+
[DriverMethod("I add {a} and {b}")]
84+
public Task Add(int a, int b)
85+
{
86+
_calculator.Add(a, b);
87+
return Task.CompletedTask;
88+
}
89+
```
90+
91+
- **Scenario Outlines** with Examples tables for parameterized tests
92+
- **YAML format** as alternative to Gherkin for tooling integration
93+
94+
### Framework Integration
95+
6496
- **Test framework adapters**:
6597

6698
* **MSTest**: `TinyBddMsTestBase`, `MSTestBddReporter`, `MSTestTraitBridge`
@@ -105,6 +137,9 @@ dotnet add package TinyBDD.Xunit.v3
105137
For Extensions:
106138

107139
```powershell
140+
# File-Based DSL (Gherkin and YAML)
141+
dotnet add package TinyBDD.Extensions.FileBased
142+
108143
# Dependency Injection
109144
dotnet add package TinyBDD.Extensions.DependencyInjection
110145
@@ -233,6 +268,109 @@ public class MathTests : TinyBddXunitBase
233268

234269
---
235270

271+
## File-Based Usage
272+
273+
### Gherkin Feature File
274+
275+
Create `Features/Calculator.feature`:
276+
277+
```gherkin
278+
Feature: Calculator Operations
279+
280+
@calculator @smoke
281+
Scenario: Add two numbers
282+
Given a calculator
283+
When I add 5 and 3
284+
Then the result should be 8
285+
286+
Scenario Outline: Multiply numbers
287+
Given a calculator
288+
When I multiply <a> and <b>
289+
Then the result should be <expected>
290+
291+
Examples:
292+
| a | b | expected |
293+
| 2 | 3 | 6 |
294+
| 4 | 5 | 20 |
295+
```
296+
297+
### Driver Implementation
298+
299+
```csharp
300+
using TinyBDD.Extensions.FileBased.Core;
301+
302+
public class CalculatorDriver : IApplicationDriver
303+
{
304+
private readonly Calculator _calculator = new();
305+
306+
[DriverMethod("a calculator")]
307+
public Task Initialize()
308+
{
309+
_calculator.Clear();
310+
return Task.CompletedTask;
311+
}
312+
313+
[DriverMethod("I add {a} and {b}")]
314+
public Task Add(int a, int b)
315+
{
316+
_calculator.Add(a, b);
317+
return Task.CompletedTask;
318+
}
319+
320+
[DriverMethod("I multiply {a} and {b}")]
321+
public Task Multiply(int a, int b)
322+
{
323+
_calculator.Multiply(a, b);
324+
return Task.CompletedTask;
325+
}
326+
327+
[DriverMethod("the result should be {expected}")]
328+
public Task<bool> VerifyResult(int expected)
329+
{
330+
return Task.FromResult(_calculator.GetResult() == expected);
331+
}
332+
333+
public Task InitializeAsync(CancellationToken ct = default) => Task.CompletedTask;
334+
public Task CleanupAsync(CancellationToken ct = default) => Task.CompletedTask;
335+
}
336+
```
337+
338+
### Test Class
339+
340+
```csharp
341+
using TinyBDD.Extensions.FileBased;
342+
343+
public class CalculatorTests : FileBasedTestBase<CalculatorDriver>
344+
{
345+
[Fact]
346+
public async Task ExecuteCalculatorScenarios()
347+
{
348+
await ExecuteScenariosAsync(options =>
349+
{
350+
options.AddFeatureFiles("Features/**/*.feature")
351+
.WithBaseDirectory(Directory.GetCurrentDirectory());
352+
});
353+
}
354+
}
355+
```
356+
357+
Output:
358+
359+
```
360+
Feature: Calculator Operations
361+
Scenario: Add two numbers
362+
Given a calculator [OK] 0 ms
363+
When I add 5 and 3 [OK] 0 ms
364+
Then the result should be 8 [OK] 1 ms
365+
366+
Scenario Outline: Multiply numbers (Example 1: a=2, b=3, expected=6)
367+
Given a calculator [OK] 0 ms
368+
When I multiply 2 and 3 [OK] 0 ms
369+
Then the result should be 6 [OK] 0 ms
370+
```
371+
372+
---
373+
236374
## Step Types
237375

238376
| Step | Purpose | Example |
@@ -328,18 +466,22 @@ This ensures that all steps passed and throws if any failed.
328466

329467
TinyBDD was created with a few guiding principles:
330468

331-
1. **Focus on readability, not ceremony**
332-
Steps should read like plain English and map directly to Gherkin-style thinking, but without requiring `.feature`
333-
files, extra compilers, or DSL preprocessors.
469+
1. **Focus on readability, not ceremony**
470+
Steps should read like plain English and map directly to Gherkin-style thinking. Choose the approach that best fits your team:
471+
- **Code-first**: Write BDD tests directly in C# with fluent API
472+
- **File-based**: Use standard Gherkin `.feature` files or YAML for business-readable specifications
473+
474+
2. **Flexible specification approach**
475+
Both approaches produce the same readable output:
476+
- **Code-first**: Your C# code using the fluent API serves as the executable specification
477+
- **File-based**: Separate `.feature` or YAML files define scenarios, implemented through driver methods
334478

335-
2. **Code is the spec**
336-
Instead of writing a separate Gherkin text file, you write directly in C# using a fluent API that mirrors `Given`
337-
`When``Then``And``But`.
338-
Your unit test runner output **is** the human-readable spec.
479+
Your test runner output **is** the human-readable spec in both cases.
339480

340-
3. **Stay out of your way**
341-
TinyBDD is not an opinionated test framework; it’s a syntax layer that integrates with MSTest, xUnit, or NUnit and
342-
leaves assertions, test discovery, and reporting to them.
481+
3. **Stay out of your way**
482+
TinyBDD is not an opinionated test framework; it's a syntax layer that integrates with MSTest, xUnit, or NUnit and
483+
leaves assertions, test discovery, and reporting to them. Choose code-first for flexibility and complex logic, or
484+
file-based when business analysts need to author test specifications.
343485

344486
---
345487

@@ -373,15 +515,23 @@ If a step fails, you’ll see exactly which step failed, how long it took, and t
373515

374516
---
375517

376-
## Why Not Use SpecFlow / Cucumber?
518+
## Why Use TinyBDD?
377519

378-
SpecFlow, Cucumber, and similar tools are powerful for large-scale BDD, but they:
520+
TinyBDD offers flexibility that traditional BDD tools don't:
379521

380-
* Require separate `.feature` files and a parser/runner.
381-
* Often introduce a disconnect between the feature file and the code that actually runs.
382-
* Come with heavier setup and slower test discovery.
522+
**Compared to SpecFlow / Cucumber:**
523+
* **Choose your approach**: Start code-first, add `.feature` files later when business analysts join, or vice versa
524+
* **No runtime overhead**: File-based DSL uses convention-based matching without reflection or runtime parsing
525+
* **Lighter setup**: Works directly with standard test frameworks (xUnit, NUnit, MSTest)
526+
* **Better IDE support**: Code-first approach gets full IntelliSense, refactoring, and debugging
527+
* **Simpler integration**: No separate test runners, no complex step binding configurations
383528

384-
TinyBDD keeps **everything in one place**—your test class—while still producing clear, human-readable steps.
529+
**Unique advantages:**
530+
* Seamlessly switch between approaches as your team's needs evolve
531+
* Both approaches produce identical readable Gherkin-style output
532+
* Test framework agnostic - use the test runner you already know
533+
* Performance-optimized with automatic source generation (code-first)
534+
* Convention-based driver methods for file-based tests (no attribute soup)
385535

386536
---
387537

TinyBDD.sln

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinyBDD.Benchmarks", "bench
4949
EndProject
5050
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinyBDD.SourceGenerators", "src\TinyBDD.SourceGenerators\TinyBDD.SourceGenerators.csproj", "{BEDB689C-B1BE-417F-97CD-6F727F3E539B}"
5151
EndProject
52+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinyBDD.Extensions.FileBased", "src\TinyBDD.Extensions.FileBased\TinyBDD.Extensions.FileBased.csproj", "{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}"
53+
EndProject
54+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TinyBDD.Extensions.FileBased.Tests", "tests\TinyBDD.Extensions.FileBased.Tests\TinyBDD.Extensions.FileBased.Tests.csproj", "{4FBA59B6-192F-4E69-B858-B6549DC3617E}"
55+
EndProject
5256
Global
5357
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5458
Debug|Any CPU = Debug|Any CPU
@@ -251,6 +255,30 @@ Global
251255
{BEDB689C-B1BE-417F-97CD-6F727F3E539B}.Release|x64.Build.0 = Release|Any CPU
252256
{BEDB689C-B1BE-417F-97CD-6F727F3E539B}.Release|x86.ActiveCfg = Release|Any CPU
253257
{BEDB689C-B1BE-417F-97CD-6F727F3E539B}.Release|x86.Build.0 = Release|Any CPU
258+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
259+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
260+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Debug|x64.ActiveCfg = Debug|Any CPU
261+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Debug|x64.Build.0 = Debug|Any CPU
262+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Debug|x86.ActiveCfg = Debug|Any CPU
263+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Debug|x86.Build.0 = Debug|Any CPU
264+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
265+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Release|Any CPU.Build.0 = Release|Any CPU
266+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Release|x64.ActiveCfg = Release|Any CPU
267+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Release|x64.Build.0 = Release|Any CPU
268+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Release|x86.ActiveCfg = Release|Any CPU
269+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9}.Release|x86.Build.0 = Release|Any CPU
270+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
271+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Debug|Any CPU.Build.0 = Debug|Any CPU
272+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Debug|x64.ActiveCfg = Debug|Any CPU
273+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Debug|x64.Build.0 = Debug|Any CPU
274+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Debug|x86.ActiveCfg = Debug|Any CPU
275+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Debug|x86.Build.0 = Debug|Any CPU
276+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Release|Any CPU.ActiveCfg = Release|Any CPU
277+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Release|Any CPU.Build.0 = Release|Any CPU
278+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Release|x64.ActiveCfg = Release|Any CPU
279+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Release|x64.Build.0 = Release|Any CPU
280+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Release|x86.ActiveCfg = Release|Any CPU
281+
{4FBA59B6-192F-4E69-B858-B6549DC3617E}.Release|x86.Build.0 = Release|Any CPU
254282
EndGlobalSection
255283
GlobalSection(SolutionProperties) = preSolution
256284
HideSolutionNode = FALSE
@@ -272,5 +300,7 @@ Global
272300
{7ED5DB8F-1FE7-486F-A0F6-F3BC7D718F1B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
273301
{E1F2A3B4-C5D6-4E7F-9A0B-1C2D3E4F5A6B} = {D1E2F3A4-B5C6-4D7E-8F9A-0B1C2D3E4F5A}
274302
{BEDB689C-B1BE-417F-97CD-6F727F3E539B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
303+
{7D0FB881-8B9B-4366-A6A5-89D2BCD9A9D9} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
304+
{4FBA59B6-192F-4E69-B858-B6549DC3617E} = {3925CEDE-958F-4576-9C2C-A53538FC0FBE}
275305
EndGlobalSection
276306
EndGlobal

0 commit comments

Comments
 (0)