https://github.com/jerrettdavis/tinybdd
TinyBDD is a minimal, fluent Behavior-Driven Development library for .NET
https://github.com/jerrettdavis/tinybdd
bdd bdd-framework bdd-tests fluent fluent-api fluent-design
Last synced: 22 days ago
JSON representation
TinyBDD is a minimal, fluent Behavior-Driven Development library for .NET
- Host: GitHub
- URL: https://github.com/jerrettdavis/tinybdd
- Owner: JerrettDavis
- License: mit
- Created: 2025-08-14T05:04:42.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-01-15T06:54:33.000Z (27 days ago)
- Last Synced: 2026-01-15T08:47:43.263Z (27 days ago)
- Topics: bdd, bdd-framework, bdd-tests, fluent, fluent-api, fluent-design
- Language: C#
- Homepage: https://jerrettdavis.github.io/TinyBDD/
- Size: 961 KB
- Stars: 2
- Watchers: 1
- Forks: 1
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# TinyBDD
[](https://github.com/JerrettDavis/TinyBDD/actions/workflows/ci.yml)
[](https://codecov.io/gh/JerrettDavis/TinyBDD)
[](https://github.com/JerrettDavis/TinyBDD/security/code-scanning)
[](LICENSE.md)

**NuGet Packages:**
| Package | Version | Downloads |
|--------|---------|-----------|
| **TinyBDD** | [](https://www.nuget.org/packages/TinyBDD/) | [](https://www.nuget.org/packages/TinyBDD/) |
| **TinyBDD.MSTest** | [](https://www.nuget.org/packages/TinyBDD.MSTest/) | [](https://www.nuget.org/packages/TinyBDD.MSTest/) |
| **TinyBDD.Xunit** | [](https://www.nuget.org/packages/TinyBDD.Xunit/) | [](https://www.nuget.org/packages/TinyBDD.Xunit/) |
| **TinyBDD.Xunit.v3** | [](https://www.nuget.org/packages/TinyBDD.Xunit.v3/) | [](https://www.nuget.org/packages/TinyBDD.Xunit.v3/) |
| **TinyBDD.NUnit** | [](https://www.nuget.org/packages/TinyBDD.NUnit/) | [](https://www.nuget.org/packages/TinyBDD.NUnit/) |
| **TinyBDD.Extensions.DependencyInjection** | [](https://www.nuget.org/packages/TinyBDD.Extensions.DependencyInjection/) | [](https://www.nuget.org/packages/TinyBDD.Extensions.DependencyInjection/) |
| **TinyBDD.Extensions.FileBased** | [](https://www.nuget.org/packages/TinyBDD.Extensions.FileBased/) | [](https://www.nuget.org/packages/TinyBDD.Extensions.FileBased/) |
| **TinyBDD.Extensions.Hosting** | [](https://www.nuget.org/packages/TinyBDD.Extensions.Hosting/) | [](https://www.nuget.org/packages/TinyBDD.Extensions.Hosting/) |
| **TinyBDD.Extensions.Reporting** | [](https://www.nuget.org/packages/TinyBDD.Extensions.Reporting/) | [](https://www.nuget.org/packages/TinyBDD.Extensions.Reporting/) |
---
**TinyBDD** is a minimal, fluent **Behavior-Driven Development** library for .NET.
It supports two complementary approaches:
- **Code-first**: Fluent `Given` / `When` / `Then` syntax directly in C#
- **File-based**: Gherkin `.feature` files and YAML scenarios with convention-based drivers
Both approaches are designed to:
- Be **framework-agnostic** (works with MSTest, xUnit, NUnit, etc.).
- Keep scenarios **clear and concise** with readable syntax.
- Support **async and sync** operations for maximum flexibility.
- Integrate with existing test runners' output for easy step visibility.
---
## Features
### Code-First Approach
- **Readable BDD syntax**:
```csharp
await Given("a number", () => 5)
.When("doubled", x => x * 2)
.Then(">= 10", v => v >= 10)
.And("<= 20", v => v <= 20)
.But("!= 15", v => v != 15)
.AssertPassed();
```
- **Sync & Async Support**:
* `Func` / `Func`
* `Func>` / `Func>`
* Token-aware variants for advanced control.
- **`And` / `But` chaining** with correct step names in output:
```
Given start [OK]
When double [OK]
Then >= 10 [OK]
And <= 20 (async) [OK]
But != 11 [OK]
```
### File-Based Approach
- **Gherkin .feature files**:
```gherkin
Feature: Calculator Operations
Scenario: Add two numbers
Given a calculator
When I add 5 and 3
Then the result should be 8
```
- **Convention-based driver methods**:
```csharp
[DriverMethod("I add {a} and {b}")]
public Task Add(int a, int b)
{
_calculator.Add(a, b);
return Task.CompletedTask;
}
```
- **Scenario Outlines** with Examples tables for parameterized tests
- **YAML format** as alternative to Gherkin for tooling integration
### Framework Integration
- **Test framework adapters**:
* **MSTest**: `TinyBddMsTestBase`, `MSTestBddReporter`, `MSTestTraitBridge`
* **xUnit**: `TinyBddXunitBase`, `XunitTraitBridge`, `XunitBddReporter`
* **NUnit**: `TinyBddNUnitBase`, `NUnitTraitBridge`, `NUnitBddReporter`
* Automatically logs steps and tags to the test output.
---
## Installation
Add TinyBDD via NuGet:
```powershell
dotnet add package TinyBDD
```
For MSTest:
```powershell
dotnet add package TinyBDD.MSTest
```
For NUnit:
```powershell
dotnet add package TinyBDD.NUnit
```
For xUnit:
```powershell
dotnet add package TinyBDD.Xunit
```
For xUnit v3:
```powershell
dotnet add package TinyBDD.Xunit.v3
```
For Extensions:
```powershell
# File-Based DSL (Gherkin and YAML)
dotnet add package TinyBDD.Extensions.FileBased
# Dependency Injection
dotnet add package TinyBDD.Extensions.DependencyInjection
# Hosting (includes DI)
dotnet add package TinyBDD.Extensions.Hosting
# JSON Reporting
dotnet add package TinyBDD.Extensions.Reporting
```
### ⚡ Performance Optimization (Automatic!)
TinyBDD includes a **Roslyn source generator** that **automatically optimizes ALL BDD tests at compile-time** starting in v1.1. No attributes needed, no configuration, no additional packages!
**Default behavior** - All BDD test methods are automatically optimized:
```csharp
// This is automatically optimized - no attribute needed!
public async Task FastScenario()
{
await Given("start", () => 42)
.When("double", x => x * 2)
.Then("equals 84", x => x == 84);
}
```
**Opt-out** - Use `[DisableOptimization]` if you need the full pipeline features:
```csharp
[DisableOptimization] // Uses standard pipeline
public async Task ScenarioWithObservers()
{
// Uses standard pipeline (observers, hooks, etc.)
await Given("start", () => 1)
.When("add", x => x + 1)
.Then("equals 2", x => x == 2);
}
```
**Performance gains:**
- **16-40x faster execution** (~814ns → ~20-50ns per step)
- **9x less memory** (2,568 bytes → ~290 bytes per scenario)
- **Zero boxing** - All values are strongly typed at compile-time
- **No runtime overhead** - Transforms to direct procedural code
- **Compile-time transformation** - Happens automatically during build
**When to opt-out:**
- ⚠️ Using IStepObserver or IScenarioObserver (not yet supported in generated code)
- ⚠️ Using BeforeStep/AfterStep hooks
- ⚠️ Complex ScenarioOptions features
- 🐛 Debugging (to step through standard pipeline)
The generator transforms fluent chains into optimized procedural code while maintaining the same readable syntax. Generated code is placed in `obj/.../generated/` for inspection.
---
## Basic Usage
### MSTest Example
```csharp
using TinyBDD.MSTest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
[Feature("Math")]
[TestClass]
public class MathTests : TinyBddMsTestBase
{
[Scenario("Doubling numbers")]
[TestMethod]
public async Task DoublingScenario()
{
await Given("start with 5", () => 5)
.When("doubled", x => x * 2)
.Then("should be 10", v => v == 10)
.AssertPassed();
}
}
```
---
### NUnit Example
```csharp
using TinyBDD.NUnit;
using NUnit.Framework;
[Feature("Math")]
public class MathTests : TinyBddNUnitBase
{
[Scenario("Doubling numbers")]
[Test]
public async Task DoublingScenario()
{
await Given("start with 5", () => 5)
.When("doubled", x => x * 2)
.Then("should be 10", v => v == 10)
.AssertPassed();
}
}
```
---
### xUnit Example
```csharp
using TinyBDD.Xunit;
using Xunit;
[Feature("Math")]
public class MathTests : TinyBddXunitBase
{
[Scenario("Doubling numbers")]
[Fact]
public async Task DoublingScenario()
{
await Given("start with 5", () => 5)
.When("doubled", x => x * 2)
.Then("should be 10", v => v == 10)
.AssertPassed();
}
}
```
---
## File-Based Usage
### Gherkin Feature File
Create `Features/Calculator.feature`:
```gherkin
Feature: Calculator Operations
@calculator @smoke
Scenario: Add two numbers
Given a calculator
When I add 5 and 3
Then the result should be 8
Scenario Outline: Multiply numbers
Given a calculator
When I multiply and
Then the result should be
Examples:
| a | b | expected |
| 2 | 3 | 6 |
| 4 | 5 | 20 |
```
### Driver Implementation
```csharp
using TinyBDD.Extensions.FileBased.Core;
public class CalculatorDriver : IApplicationDriver
{
private readonly Calculator _calculator = new();
[DriverMethod("a calculator")]
public Task Initialize()
{
_calculator.Clear();
return Task.CompletedTask;
}
[DriverMethod("I add {a} and {b}")]
public Task Add(int a, int b)
{
_calculator.Add(a, b);
return Task.CompletedTask;
}
[DriverMethod("I multiply {a} and {b}")]
public Task Multiply(int a, int b)
{
_calculator.Multiply(a, b);
return Task.CompletedTask;
}
[DriverMethod("the result should be {expected}")]
public Task VerifyResult(int expected)
{
return Task.FromResult(_calculator.GetResult() == expected);
}
public Task InitializeAsync(CancellationToken ct = default) => Task.CompletedTask;
public Task CleanupAsync(CancellationToken ct = default) => Task.CompletedTask;
}
```
### Test Class
```csharp
using TinyBDD.Extensions.FileBased;
public class CalculatorTests : FileBasedTestBase
{
[Fact]
public async Task ExecuteCalculatorScenarios()
{
await ExecuteScenariosAsync(options =>
{
options.AddFeatureFiles("Features/**/*.feature")
.WithBaseDirectory(Directory.GetCurrentDirectory());
});
}
}
```
Output:
```
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] 1 ms
Scenario Outline: Multiply numbers (Example 1: a=2, b=3, expected=6)
Given a calculator [OK] 0 ms
When I multiply 2 and 3 [OK] 0 ms
Then the result should be 6 [OK] 0 ms
```
---
## Step Types
| Step | Purpose | Example |
|---------|---------------------------------------------|--------------------------------|
| `Given` | Initial state / setup | `.Given("start", () => 5)` |
| `When` | Action / event | `.When("doubled", x => x * 2)` |
| `Then` | Assertion | `.Then(">= 10", v => v >= 10)` |
| `And` | Additional assertion after `Then` or `When` | `.And("<= 20", v => v <= 20)` |
| `But` | Additional assertion phrased negatively | `.But("!= 15", v => v != 15)` |
All step types have **sync** and **async** overloads.
---
## Cleanup with Finally
`Finally` registers cleanup handlers that execute after all steps complete, even if steps throw exceptions. This is useful for resource cleanup like disposing objects:
```csharp
await Given("a database connection", () => new SqlConnection(connectionString))
.Finally("close connection", conn => conn.Dispose())
.When("query data", conn => conn.Query("SELECT * FROM Users"))
.Then("results returned", users => users.Any())
.AssertPassed();
// Connection is automatically disposed after all steps complete
```
**Key Features:**
- Finally handlers execute in registration order after all other steps
- They execute even when steps throw exceptions
- Multiple Finally handlers can be registered at different points in the chain
- Each Finally handler receives the state value at the point where it was registered
- The chain passes through the upstream value unchanged (tap semantics)
```csharp
await Given("resource A", () => new ResourceA())
.Finally("cleanup A", a => a.Dispose())
.When("create resource B", a => new ResourceB(a))
.Finally("cleanup B", b => b.Dispose())
.Then("verify", b => b.IsValid)
.AssertPassed();
// Execution order: Given → When → Then → Finally cleanup A → Finally cleanup B
```
---
## Tags
Tags can be added for reporting and filtering:
```csharp
ctx.AddTag("smoke");
ctx.AddTag("fast");
```
In xUnit, tags are logged to the test output:
```
[TinyBDD] Tag: smoke
[TinyBDD] Tag: fast
```
---
## Asserting Pass/Fail
TinyBDD tracks step results internally. At the end of the scenario, call one of the following methods:
```csharp
Scenario.AssertPassed();
Scenario.AssertFailed();
// or use the fluent syntax:
await Given("one", () => 1)
.When("add one", x => x + 1)
.Then("equals two", v => v == 2)
.AssertPassed();
await Given("one", () => 1)
.When("add one", x => x + 1)
.Then("equals elevent", v => v == 11)
.AssertFailed();
```
This ensures that all steps passed and throws if any failed.
---
## Philosophy
TinyBDD was created with a few guiding principles:
1. **Focus on readability, not ceremony**
Steps should read like plain English and map directly to Gherkin-style thinking. Choose the approach that best fits your team:
- **Code-first**: Write BDD tests directly in C# with fluent API
- **File-based**: Use standard Gherkin `.feature` files or YAML for business-readable specifications
2. **Flexible specification approach**
Both approaches produce the same readable output:
- **Code-first**: Your C# code using the fluent API serves as the executable specification
- **File-based**: Separate `.feature` or YAML files define scenarios, implemented through driver methods
Your test runner output **is** the human-readable spec in both cases.
3. **Stay out of your way**
TinyBDD is not an opinionated test framework; it's a syntax layer that integrates with MSTest, xUnit, or NUnit and
leaves assertions, test discovery, and reporting to them. Choose code-first for flexibility and complex logic, or
file-based when business analysts need to author test specifications.
---
## Gherkin-Style Output
When running a scenario, TinyBDD prints structured step output similar to Gherkin formatting.
For example:
```csharp
await Given("start", () => 5)
.When("double", x => x * 2)
.Then(">= 10", v => v >= 10)
.And("<= 20 (async)", v => Task.FromResult(v <= 20))
.But("!= 11", v => v != 11)
.AssertPassed();
```
Test output:
```
Feature: Math
Scenario: Doubling numbers
Given start [OK] 0 ms
When double [OK] 0 ms
Then >= 10 [OK] 0 ms
And <= 20 (async) [OK] 0 ms
But != 11 [OK] 0 ms
```
If a step fails, you’ll see exactly which step failed, how long it took, and the exception message.
---
## Why Use TinyBDD?
TinyBDD offers flexibility that traditional BDD tools don't:
**Compared to SpecFlow / Cucumber:**
* **Choose your approach**: Start code-first, add `.feature` files later when business analysts join, or vice versa
* **No runtime overhead**: File-based DSL uses convention-based matching without reflection or runtime parsing
* **Lighter setup**: Works directly with standard test frameworks (xUnit, NUnit, MSTest)
* **Better IDE support**: Code-first approach gets full IntelliSense, refactoring, and debugging
* **Simpler integration**: No separate test runners, no complex step binding configurations
**Unique advantages:**
* Seamlessly switch between approaches as your team's needs evolve
* Both approaches produce identical readable Gherkin-style output
* Test framework agnostic - use the test runner you already know
* Performance-optimized with automatic source generation (code-first)
* Convention-based driver methods for file-based tests (no attribute soup)
---
## Minimal Example
For the smallest possible test:
```csharp
await Given("one", () => 1)
.When("add one", x => x + 1)
.Then("equals two", v => v == 2)
.AssertPassed();
```
Output:
```
Given one [OK]
When add one [OK]
Then equals two [OK]
```
---
## Async Philosophy
In TinyBDD, sync and async steps are **equally first-class** citizens.
* If your step is synchronous, write it synchronously:
```csharp
.When("double", x => x * 2)
.Then("is 10", v => v == 10)
```
* If your step needs async work:
```csharp
.When("fetch from DB", async x => await db.GetAsync(x))
.Then("result exists", async v => Assert.NotNull(v))
```
You can even mix sync and async steps freely in the same scenario.
---
## Output Style
TinyBDD always prints the BDD keyword for the step type (`Given`, `When`, `Then`, `And`, `But`), the step title, the
result `[OK]` / `[FAIL]`, and the elapsed time in milliseconds.
For failed steps, TinyBDD stops the scenario immediately and prints the exception:
```
Then equals two [FAIL] 1 ms
Expected: 2
Actual: 3
```
---
## Recommended Usage
* One **scenario** per test method.
* Keep each step **single-purpose**—avoid hiding multiple unrelated actions in one step.
* Prefer creating functions, even local ones, to avoid unnecessary allocations, closure creation, garbage collection, and code cleanliness.
* Use **`Scenario.AssertPassed()`** or the fluent **`ThenChain.AssertPassed()`** at the end of each test to ensure every step was explicitly checked.
* Use **tags** to group and filter tests.
----
## License
[MIT License](LICENSE)