SpecFlow C# Test Automation Guide (2026)
The 2026 SpecFlow C# tutorial — set up SpecFlow + .NET 8 + NUnit + Selenium, write production Gherkin feature files, C# step definitions, table transformations, hooks, LivingDoc and CI/CD.

What you'll master: a working SpecFlow + .NET 8 + NUnit + Selenium setup, production-quality Gherkin feature files bound to C# step definitions, table transformations for clean data binding, hooks for setup/teardown, and a CI/CD-ready pipeline.
1. What Is SpecFlow?
SpecFlow is the .NET port of Cucumber — it brings Behavior-Driven Development (BDD) to the Microsoft ecosystem: C#, .NET, Visual Studio and Azure DevOps. After Tricentis acquired SpecFlow in 2020 the project has continued to ship, and SpecFlow 3.9+ supports .NET 6, 7, 8 and 9.
SpecFlow remains the default BDD choice for .NET shops because of:
- Native .NET integration with NUnit, xUnit and MSTest
- Visual Studio + ReSharper / Rider first-class IDE experience with Gherkin IntelliSense and live step linking
- Living documentation via SpecFlow+ LivingDoc
- Azure DevOps, MSBuild and TFS integration out of the box
For broader BDD context, pair this guide with our BDD Cucumber tutorial and the dedicated SpecFlow interview questions hub.
🚀 Skip the boilerplate. Rehearse SpecFlow rounds live with our AI mock interview and review Selenium interview questions alongside the BDD prep.
2. SpecFlow vs Cucumber
| Dimension | SpecFlow | Cucumber-JVM |
|---|---|---|
| Language | C# / VB.NET | Java |
| Test runner | NUnit / xUnit / MSTest | JUnit / TestNG |
| IDE support | Visual Studio + ReSharper | IntelliJ + Eclipse |
| Living docs | SpecFlow+ LivingDoc | ClueCumber, Cucumber Reports |
| Ecosystem | Microsoft / Azure DevOps | Java / Maven / Gradle |
| Best for | .NET shops | Java shops |
Both frameworks use the same Gherkin syntax, so scenarios port between them with minimal change. For a Java + Cucumber alternative, see our Java for Selenium Automation guide.
3. Install SpecFlow Step by Step
Prerequisites
- .NET 8 SDK (recommended) or .NET 9 SDK
- Visual Studio 2022 or JetBrains Rider
- SpecFlow extension for Visual Studio (free, from the Marketplace)
Step 1 — Create a project
dotnet new nunit -n SpecFlowDemo
cd SpecFlowDemoStep 2 — Add SpecFlow packages
dotnet add package SpecFlow
dotnet add package SpecFlow.NUnit
dotnet add package SpecFlow.Tools.MsBuild.Generation
# Selenium integration
dotnet add package Selenium.WebDriver
dotnet add package Selenium.SupportStep 3 — Configure SpecFlow in .csproj
<ItemGroup>
<PackageReference Include="SpecFlow" Version="3.9.7" />
<PackageReference Include="SpecFlow.NUnit" Version="3.9.7" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.9.7">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>Step 4 — Verify the install
dotnet build
dotnet testProject structure
SpecFlowDemo/
├── Features/
│ └── Login.feature
├── StepDefinitions/
│ └── LoginSteps.cs
├── Pages/
│ ├── BasePage.cs
│ └── LoginPage.cs
├── Hooks/
│ └── ScenarioHooks.cs
├── Drivers/
│ └── DriverFactory.cs
└── SpecFlowDemo.csproj4. Your First Feature File
Create Features/Login.feature:
Feature: User login
As a registered user
I want to log in to the application
So that I can access my account
Background:
Given the application is running
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter "admin@example.com" as the email
And I enter "Sup3rSecret!" as the password
And I click the "Sign in" button
Then I should be on the dashboard page
And I should see "Welcome, admin"
Scenario: Failed login with invalid password
Given I am on the login page
When I enter "admin@example.com" as the email
And I enter "wrong-password" as the password
And I click the "Sign in" button
Then I should see an error message "Invalid credentials"In Visual Studio, right-click the .feature file → Generate Step Definitions. SpecFlow scaffolds a stub class for every step.
5. Step Definitions in C#
Create StepDefinitions/LoginSteps.cs:
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using SeleniumExtras.WaitHelpers;
using TechTalk.SpecFlow;
using SpecFlowDemo.Pages;
namespace SpecFlowDemo.StepDefinitions
{
[Binding]
public class LoginSteps
{
private readonly ScenarioContext _scenarioContext;
private readonly IWebDriver _driver;
private readonly LoginPage _loginPage;
public LoginSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
_driver = (IWebDriver)_scenarioContext["driver"];
_loginPage = new LoginPage(_driver);
}
[Given(@"I am on the login page")]
public void GivenIAmOnTheLoginPage() => _loginPage.Navigate();
[When(@"I enter ""([^""]*)"" as the email")]
public void WhenIEnterEmail(string email) => _loginPage.EnterEmail(email);
[When(@"I enter ""([^""]*)"" as the password")]
public void WhenIEnterPassword(string password) => _loginPage.EnterPassword(password);
[When(@"I click the ""([^""]*)"" button")]
public void WhenIClickButton(string label) =>
_driver.FindElement(By.CssSelector("button[type='submit']")).Click();
[Then(@"I should be on the dashboard page")]
public void ThenIShouldBeOnTheDashboardPage()
{
var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
wait.Until(ExpectedConditions.UrlContains("/dashboard"));
Assert.That(_driver.Url, Does.Contain("/dashboard"));
}
[Then(@"I should see ""([^""]*)""")]
public void ThenIShouldSee(string text) =>
Assert.That(_driver.PageSource, Does.Contain(text));
}
}The [Binding] attribute tells SpecFlow this class contains step definitions. Each method has a [Given], [When] or [Then] attribute with a regex that matches the Gherkin step.
6. Scenario Outlines and Examples
Run the same scenario against multiple datasets:
Scenario Outline: Login with multiple users
Given I am on the login page
When I enter "<email>" as the email
And I enter "<password>" as the password
And I click the "Sign in" button
Then I should see "<greeting>"
Examples:
| email | password | greeting |
| admin@example.com | Sup3rSecret! | Welcome, admin |
| viewer@example.com | ViewerPass1! | Welcome, viewer |
| guest@example.com | GuestPass1! | Welcome, guest |SpecFlow runs this scenario once per row. Step definitions are identical to a regular scenario — SpecFlow substitutes the placeholders automatically.
7. Data Tables and Table Transformations
Basic DataTable usage
Scenario: Apply multiple promo codes
Given I am on the cart page
When I apply the following promo codes:
| code | expected_discount |
| WELCOME10 | 10% |
| SAVE20 | 20% |
| VIP30 | 30% |
Then I should see the total discount applied[When(@"I apply the following promo codes:")]
public void WhenIApplyPromoCodes(Table table)
{
foreach (var row in table.Rows)
{
var code = row["code"];
var expectedDiscount = row["expected_discount"];
// Apply code, verify expected_discount
}
}Table transformations (the killer feature)
public class PromoCode
{
public string Code { get; set; }
public string ExpectedDiscount { get; set; }
}
[Binding]
public class PromoCodeTransforms
{
[StepArgumentTransformation]
public IEnumerable<PromoCode> TransformPromoCodes(Table table) =>
table.CreateSet<PromoCode>();
}
[When(@"I apply the following promo codes:")]
public void WhenIApplyPromoCodes(IEnumerable<PromoCode> promoCodes)
{
foreach (var promo in promoCodes)
{
// Apply promo.Code, verify promo.ExpectedDiscount
}
}This eliminates the foreach (var row in table.Rows) boilerplate and gives you strongly-typed access.
Vertical data tables
Scenario: Verify user profile fields
Given I am on the user profile page
Then the user profile should show:
| field | value |
| name | Alice Johnson |
| email | alice@ex.com |
| role | admin |
| department | Engineering |8. Hooks and Step Argument Transformations
Hooks
[Binding]
public class ScenarioHooks
{
private readonly ScenarioContext _scenarioContext;
public ScenarioHooks(ScenarioContext scenarioContext)
=> _scenarioContext = scenarioContext;
[BeforeScenario]
public void BeforeScenario() =>
_scenarioContext["driver"] = new ChromeDriver();
[BeforeScenario("@database")]
public void BeforeDatabaseScenario() => DatabaseHelper.ResetDatabase();
[AfterScenario]
public void AfterScenario()
{
var driver = (IWebDriver)_scenarioContext["driver"];
driver?.Quit();
}
[BeforeTestRun]
public static void BeforeTestRun() { /* once before all tests */ }
[AfterTestRun]
public static void AfterTestRun() { /* once after all tests */ }
}Step argument transformations
[Binding]
public class Transforms
{
[StepArgumentTransformation(@"(\d+) days? ago")]
public DateTime DaysAgoTransform(int days) =>
DateTime.UtcNow.AddDays(-days);
[StepArgumentTransformation(@"user ""([^""]+)""")]
public User UserTransform(string email) =>
UserRepository.FindByEmail(email);
}Now your Gherkin can say:
Given the user "admin@example.com" registered 30 days agoAnd the step definition accepts User and DateTime directly — no manual parsing.
9. SpecFlow + Selenium Integration
Driver factory
namespace SpecFlowDemo.Drivers
{
public static class DriverFactory
{
public static IWebDriver CreateDriver(string browser) => browser.ToLower() switch
{
"chrome" => new ChromeDriver(),
"firefox" => new FirefoxDriver(),
"edge" => new EdgeDriver(),
_ => throw new ArgumentException($"Unknown browser: {browser}")
};
}
}Page object (POM adapted for SpecFlow)
namespace SpecFlowDemo.Pages
{
public class LoginPage
{
private readonly IWebDriver _driver;
public LoginPage(IWebDriver driver) => _driver = driver;
public void Navigate() =>
_driver.Navigate().GoToUrl("https://example.com/login");
public void EnterEmail(string email) =>
_driver.FindElement(By.Id("email")).SendKeys(email);
public void EnterPassword(string password) =>
_driver.FindElement(By.Id("password")).SendKeys(password);
public void ClickSignIn() =>
_driver.FindElement(By.Id("submit")).Click();
}
}For the full Selenium POM pattern, see our Selenium WebDriver guide and the Java for Selenium deep-dive.
10. Living Documentation with SpecFlow+
SpecFlow+ LivingDoc generates human-readable HTML documentation from your feature files — the BDD payoff. Scenarios become the executable specification of your system.
Setup
dotnet add package SpecFlow.Plus.LivingDocPlugin
dotnet add package SpecFlow.Plus.LivingDocGenerate the docs
livingdoc test-assembly SpecFlowDemo.dll -t TestExecution.jsonYou get an HTML site with a navigable tree of feature files, scenario results, pass/fail trends, execution times and tag filters.
Publish to Azure DevOps / GitHub Pages
# Azure DevOps
- task: CopyFiles@2
inputs:
SourceFolder: '$(Build.ArtifactStagingDirectory)/livingdoc'
TargetFolder: '$(Build.ArtifactStagingDirectory)/drop'
- task: PublishBuildArtifacts@1
inputs:
PathToPublish: '$(Build.ArtifactStagingDirectory)/drop'11. CI/CD Integration Patterns
GitHub Actions
name: SpecFlow Tests
on: [push, pull_request]
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- run: dotnet restore
- run: dotnet build --no-restore
- run: dotnet test --no-build --logger "trx;LogFileName=test-results.trx"
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: test-results
path: TestResults/Azure DevOps
trigger:
- main
pool:
vmImage: 'windows-latest'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'test'
arguments: '--logger trx --collect:"XPlat Code Coverage"'
- task: PublishTestResults@2
inputs:
testRunner: 'VSTest'
testResultsFiles: '**/*.trx'Parallel execution
SpecFlow runs in parallel via NUnit's Parallelizable attribute:
[assembly: Parallelizable(ParallelScope.Fixtures)]Each feature file runs in parallel — use carefully, parallel features can't share state.
12. SpecFlow Best Practices
Do
- Use Visual Studio + ReSharper (or Rider) for the best BDD experience
- Generate LivingDoc for every release
- Use Page Object Model for clean step definitions
- Use table transformations for typed data binding
- Use Scenario Outlines for data-driven scenarios
- Use tags to organize by feature, priority and test type
- Use ScenarioContext sparingly — only for driver and minimal cross-step state
- Pair SpecFlow with NUnit for the richest test runner features
Don't
- Don't put business logic in step definitions — they should be thin glue
- Don't write imperative scenarios — use declarative language
- Don't use
ScenarioContext.Current(deprecated in SpecFlow 3) — use constructor injection - Don't share driver instances across scenarios without cleanup
- Don't run scenarios in parallel without isolation
- Don't skip the discovery workshop — BDD without collaboration is keyword-driven testing