Overview
Per reqnroll-home (opens in new window):
"Reqnroll is described as 'an open-source Cucumber-style BDD test automation framework for .NET. It has been created as a reboot of the SpecFlow project.'"
The "reboot" framing is the key: SpecFlow's maintenance slowed in 2023; the community forked into Reqnroll, which has continued active development.
When to use
For SpecFlow-locked legacy projects mid-migration (packages, bindings, EOL background), see references/specflow-legacy.md.
Worked example
A checkout team adds one BDD scenario, "Apply valid promo," to a new xUnit project.
Step 1 - Install
# In the test project directory
dotnet add package Reqnroll.xUnit # or Reqnroll.NUnit / Reqnroll.MsTest
dotnet add package Reqnroll.Tools.MsBuild.Generation # generates code from .feature filesPer reqnroll-home (opens in new window): "Works across common operating systems and .NET versions (including .NET 8.0)."
Verify: dotnet build succeeds and restores both packages before you author features. If code generation does not run, confirm Reqnroll.Tools.MsBuild.Generation is referenced in the test project.
Step 2 - Author a Feature
# Features/Cart.feature
Feature: Apply promo code at checkout
Background:
Given a logged-in user with email confirmed
And the cart contains 1 of "BOOK-001" at $24.99
Rule: Promo codes apply only when valid
Scenario: Apply valid promo
When I enter "WELCOME10" in the promo input
And I click "Apply"
Then the subtotal updates to $22.49
Scenario Outline: Reject invalid codes
When I enter "<code>" in the promo input
And I click "Apply"
Then an error appears: "<error>"
Examples:
| code | error |
| EXPIRED50 | This code has expired |
| NOTREAL | Code not found |Rule: blocks (Gherkin 6+) group related scenarios; per reqnroll-home (opens in new window) this is supported.
Step 3 - Step bindings
Write step bindings as [Given/When/Then]-decorated methods in a [Binding] class. Per reqnroll-home (opens in new window): "Supports flexible step definitions using regex or cucumber expressions" - regex is more flexible, cucumber expressions ({int}, {string}, {double}) more readable. Full CartSteps binding class and the cucumber-expression variant: references/bindings-and-hooks.md.
Step 4 - Async support
Per reqnroll-home (opens in new window): "Async step definitions and hooks."
[Then("the order arrives within (\\d+) minutes")]
public async Task ThenOrderArrives(int minutes)
{
await EmailInbox.WaitForOrderConfirmation(TimeSpan.FromMinutes(minutes));
}async Task step methods work transparently; no special config.
Step 5 - Hooks
Use [BeforeTestRun], [BeforeScenario] / [AfterScenario], and tag-scoped hooks (e.g. [BeforeScenario("@browser")]) for setup and teardown; wrap each scenario in a transaction and roll back in [AfterScenario] so state does not leak between scenarios. Full hook class: references/bindings-and-hooks.md.
Step 6 - Tags
@critical @regression
Scenario: Apply valid promo
...
@browser @wip
Scenario: New checkout flow
...dotnet test --filter "Category=critical"
dotnet test --filter "Category!=wip"Step 7 - Run
# All tests
dotnet test
# Specific feature
dotnet test --filter "FullyQualifiedName~Cart"
# Generate JUnit XML for CI
dotnet test --logger "junit;LogFilePath=reports/test-results.xml"If a scenario fails or reports an undefined step, fix the matching binding in the [Binding] class and re-run until it turns green.
Step 8 - IDE support
Per reqnroll-home (opens in new window): "IDE support for Visual Studio 2022, VS Code, and Rider."
The Reqnroll plugin enables:
Step 9 - Migrate from SpecFlow
Per reqnroll-home (opens in new window): "Compatible with SpecFlow, allowing quick migration of existing projects."
Migration path:
Most SpecFlow projects migrate in <1 day for typical scope.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Starting new .NET BDD with SpecFlow in 2026+ | SpecFlow's maintenance has slowed; Reqnroll is the active fork. | Pick Reqnroll (Step 1). |
| Mixing SpecFlow + Reqnroll in one solution | Two BDD runners; double maintenance. | Migrate everything (Step 9). |
| Regex-only steps when cucumber expressions would work | Less readable; harder to maintain. | Cucumber expressions for typical cases (Step 3). |
No [BeforeScenario] cleanup | State leaks between scenarios. | Per-scenario hook + transaction rollback (Step 5). |
| Sync step methods that block on async | Deadlocks in xUnit / NUnit / MsTest. | async Task step methods (Step 4). |
Limitations
References
Full step-binding and hook examples for reqnroll-testing (opens in new window) (Steps 3 and 5).
Step bindings
// Steps/CartSteps.cs
using Reqnroll;
using Xunit;
[Binding]
public class CartSteps
{
private CheckoutPage _page;
private Cart _cart;
[Given("a logged-in user with email confirmed")]
public async Task GivenLoggedInUser()
{
var user = await TestUsers.LoggedInWithEmailConfirmed();
_page = new CheckoutPage(user);
}
[Given(@"the cart contains (\d+) of ""([^""]*)"" at \$(\d+\.\d+)")]
public void GivenCartContains(int qty, string sku, decimal price)
{
_cart = new Cart();
_cart.AddItem(new Item(sku, qty, price));
_page.SetCart(_cart);
}
[When(@"I enter ""([^""]*)"" in the promo input")]
public async Task WhenIEnter(string code)
{
await _page.EnterPromoAsync(code);
}
[When(@"I click ""([^""]*)""")]
public async Task WhenIClick(string label)
{
await _page.ClickAsync(label);
}
[Then(@"the subtotal updates to \$(\d+\.\d+)")]
public void ThenSubtotalUpdates(decimal expected)
{
Assert.Equal(expected, _page.GetSubtotal(), 2);
}
}Per reqnroll-home (opens in new window): "Supports flexible step definitions using regex or cucumber expressions." The example uses regex; cucumber expressions are an alternative:
[Given("the cart contains {int} of {string} at ${double}")]
public void GivenCartContains(int qty, string sku, double price) { ... }Cucumber expressions are more readable; regex is more flexible.
Hooks
using Reqnroll;
[Binding]
public class TestHooks
{
[BeforeTestRun]
public static async Task BeforeTestRun()
{
// Once per test run
await TestDatabase.Initialize();
}
[BeforeScenario]
public async Task BeforeScenario()
{
// Per-scenario
await TestDatabase.StartTransaction();
}
[AfterScenario]
public async Task AfterScenario(ScenarioContext context)
{
await TestDatabase.Rollback();
if (context.TestError is not null)
{
await ScreenshotCapture.Capture(context.ScenarioInfo.Title);
}
}
[BeforeScenario("@browser")]
public async Task BeforeBrowserScenario()
{
// Tag-scoped hook
await Browser.LaunchAsync();
}
}Source
SpecFlow legacy support and migration to Reqnroll
View source (opens in a new tab) (opens in new window)Deep reference for reqnroll-testing. Consult when maintaining an existing SpecFlow project that has not migrated yet, or when executing the migration.
SpecFlow is end-of-life
SpecFlow was the standard .NET BDD runner for a decade. It is dead: Tricentis, which owned it, states "SpecFlow has been retired" (shiftsync.tricentis.com (opens in new window)), specflow.org redirects there, and it "reached its end-of-life on December 31, 2024" with the GitHub projects deleted as of 1 January (reqnroll.net (opens in new window)). The packages still install only because nuget.org will not delete existing ones - exactly how newcomers land on an unsupported dependency. New .NET BDD work targets Reqnroll; use this page only for existing SpecFlow projects, especially mid-migration.
Maintaining an existing SpecFlow project (legacy)
Package references (legacy):
<PackageReference Include="SpecFlow" Version="3.9.74" />
<PackageReference Include="SpecFlow.xUnit" Version="3.9.74" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.9.74" />Features are the same Gherkin as Reqnroll / Cucumber. Step bindings:
using TechTalk.SpecFlow;
using Xunit;
[Binding]
public class CartSteps
{
[Given("a logged-in user")]
public void GivenLoggedInUser() { /* ... */ }
[When(@"I enter ""([^""]*)"" in the promo input")]
public void WhenIEnter(string code) { /* ... */ }
[Then(@"the subtotal updates to \$(\d+\.\d+)")]
public void ThenSubtotalUpdates(decimal expected) { /* ... */ }
}Compare to Reqnroll: using TechTalk.SpecFlow → using Reqnroll; decorators identical. Running is the same dotnet test (the runner is the .NET test framework - xUnit / NUnit / MsTest).
Migration path
Per reqnroll.net (opens in new window): "Compatible with SpecFlow, allowing quick migration of existing projects."
# 1. Remove SpecFlow packages
dotnet remove package SpecFlow
dotnet remove package SpecFlow.xUnit
dotnet remove package SpecFlow.Tools.MsBuild.Generation
# 2. Add Reqnroll equivalents
dotnet add package Reqnroll.xUnit
dotnet add package Reqnroll.Tools.MsBuild.Generation// 3. Find/replace in code:
// using TechTalk.SpecFlow → using Reqnroll
// TechTalk.SpecFlow → Reqnroll# 4. Run tests; fix any breakages
dotnet testMost projects migrate in under a day; the migration is mostly mechanical.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Starting new .NET BDD with SpecFlow | Reqnroll is the actively-maintained successor; SpecFlow is EOL | Use the main reqnroll-testing skill |
| Postponing migration indefinitely | SpecFlow falls further behind .NET / IDE support | Migrate now; the cost grows over time |
| Mixing SpecFlow + Reqnroll in one solution | Two runners; conflicts | All-or-nothing migration |