SpecFlow: BDD for .NET with Gherkin and C#

SpecFlow: BDD for .NET with Gherkin and C#

SpecFlow is the leading BDD framework for .NET, letting teams write Gherkin feature files and execute them through C# step bindings. It integrates with NUnit, MSTest, and xUnit, supports full dependency injection via built-in or external DI containers, and ships with a Visual Studio / Rider extension for a smooth developer experience.

Key Takeaways

SpecFlow is a Gherkin executor for .NET. It converts plain-English Gherkin scenarios into C# test methods that run inside your existing test runner.

[Binding] classes connect Gherkin to code. The [Binding] attribute marks a class as a step-definition container; [Given], [When], and [Then] attributes map phrases to methods.

Hooks control lifecycle events. [BeforeScenario], [AfterScenario], [BeforeFeature], and [AfterFeature] hooks let you set up and tear down test state at the right granularity.

Table parameters model structured data. SpecFlow's Table type and the CreateInstance<T> / CreateSet<T> helper methods turn Gherkin tables into typed C# objects with zero boilerplate.

SpecFlow+ Runner adds parallelism and reporting. The commercial runner adds parallel execution, advanced retry logic, and rich HTML reports on top of the open-source core.

What Is SpecFlow?

SpecFlow is an open-source BDD framework for .NET that implements the Gherkin language. It works as a pre-compilation step: SpecFlow reads your .feature files and generates C# test methods that delegate to your step-binding classes. The generated methods are then executed by the test runner of your choice — NUnit, MSTest, or xUnit.

Because SpecFlow generates standard test classes, your existing CI pipeline, test result reporting, and IDE tooling all work without modification. The only addition is the SpecFlow NuGet package and, optionally, the IDE extension for .feature file editing support.

SpecFlow vs Reqnroll

In 2024 the SpecFlow project was forked by the community as Reqnroll after Tricentis reduced investment in the open-source version. Reqnroll is API-compatible with SpecFlow 3.x. Everything in this guide applies to both; Reqnroll simply replaces the SpecFlow.* NuGet package names with Reqnroll.*.

Installation

Add the SpecFlow packages via NuGet. The exact runner package depends on your test framework:

<!-- .csproj — NUnit example -->
<PackageReference Include="SpecFlow" Version="3.9.74" />
<PackageReference Include="SpecFlow.NUnit" Version="3.9.74" />
<PackageReference Include="NUnit" Version="3.14.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />

<!-- xUnit alternative -->
<PackageReference Include="SpecFlow.xUnit" Version="3.9.74" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />

Install the SpecFlow for Visual Studio or SpecFlow for JetBrains Rider extension from the respective marketplace. The extension provides .feature file syntax highlighting, step navigation (Ctrl+Click to jump from a step text to its binding), and missing-step generation.

Writing a Feature File

Create a Features/ directory in your test project and add a .feature file:

# Features/OrderCheckout.feature
Feature: Order Checkout
  As a registered customer
  I want to complete a purchase
  So that my items are shipped to me

  Background:
    Given I am logged in as customer "alice@example.com"

  Scenario: Successful checkout with credit card
    Given my cart contains:
      | Product         | Quantity | Unit Price |
      | Mechanical Keyboard | 1    | 149.99     |
      | USB-C Hub       | 2        | 34.99      |
    When I proceed to checkout
    And I pay with credit card "4111111111111111"
    Then my order should be confirmed
    And I should receive an order confirmation email

  Scenario: Checkout fails when item is out of stock
    Given "Mechanical Keyboard" is out of stock
    And my cart contains:
      | Product             | Quantity | Unit Price |
      | Mechanical Keyboard | 1        | 149.99     |
    When I proceed to checkout
    Then I should see the error "Item out of stock"
    And my order should not be placed

Background: steps run before every scenario in the feature, equivalent to a [BeforeScenario] hook scoped to the feature.

Implementing Step Bindings

Create a C# class decorated with [Binding]:

// StepDefinitions/CheckoutSteps.cs
using SpecFlow.Assist;
using TechTalk.SpecFlow;

[Binding]
public class CheckoutSteps
{
    private readonly IOrderService _orderService;
    private readonly ICartService _cartService;
    private readonly ScenarioContext _scenarioContext;

    // SpecFlow's built-in DI injects services registered in the container
    public CheckoutSteps(
        IOrderService orderService,
        ICartService cartService,
        ScenarioContext scenarioContext)
    {
        _orderService = orderService;
        _cartService = cartService;
        _scenarioContext = scenarioContext;
    }

    [Given(@"my cart contains:")]
    public void GivenMyCartContains(Table table)
    {
        var items = table.CreateSet<CartItem>();
        foreach (var item in items)
            _cartService.Add(item);
    }

    [When(@"I proceed to checkout")]
    public void WhenIProceedToCheckout()
    {
        var result = _orderService.Checkout(_cartService.GetCart());
        _scenarioContext["CheckoutResult"] = result;
    }

    [When(@"I pay with credit card ""(.*)""")]
    public void WhenIPayWithCreditCard(string cardNumber)
    {
        var result = (CheckoutResult)_scenarioContext["CheckoutResult"];
        result.PayWith(new CreditCard(cardNumber));
    }

    [Then(@"my order should be confirmed")]
    public void ThenMyOrderShouldBeConfirmed()
    {
        var result = (CheckoutResult)_scenarioContext["CheckoutResult"];
        Assert.That(result.Status, Is.EqualTo(OrderStatus.Confirmed));
    }

    [Then(@"I should see the error ""(.*)""")]
    public void ThenIShouldSeeTheError(string expectedError)
    {
        var result = (CheckoutResult)_scenarioContext["CheckoutResult"];
        Assert.That(result.Error, Is.EqualTo(expectedError));
    }
}

The regex inside [Given(...)] captures groups and passes them as method parameters. SpecFlow automatically converts captured strings to the parameter type declared in the method signature.

Dependency Injection

SpecFlow has a built-in IoC container. Any class decorated with [Binding] can declare constructor parameters, and SpecFlow will instantiate and inject them. For shared state between step definition classes in the same scenario, register a context object:

// Support/ScenarioState.cs
public class ScenarioState
{
    public User CurrentUser { get; set; }
    public Order PlacedOrder { get; set; }
    public string LastError { get; set; }
}

// Inject in multiple step classes
[Binding]
public class AuthSteps
{
    private readonly ScenarioState _state;
    private readonly IAuthService _auth;

    public AuthSteps(ScenarioState state, IAuthService auth)
    {
        _state = state;
        _auth = auth;
    }

    [Given(@"I am logged in as customer ""(.*)""")]
    public async Task GivenIAmLoggedInAsCustomer(string email)
    {
        _state.CurrentUser = await _auth.LoginAsync(email, "password");
    }
}

For projects using an external DI framework (Microsoft.Extensions.DependencyInjection, Autofac, etc.), SpecFlow provides adapter packages:

<PackageReference Include="SpecFlow.Autofac" Version="3.9.22" />

Hooks

Hooks let you run code at specific points in the test lifecycle:

[Binding]
public class Hooks
{
    private readonly IDatabase _db;
    private IDbTransaction _transaction;

    public Hooks(IDatabase db) => _db = db;

    [BeforeScenario]
    public void BeforeScenario()
    {
        // Open a transaction; rolled back in AfterScenario for test isolation
        _transaction = _db.BeginTransaction();
    }

    [AfterScenario]
    public void AfterScenario()
    {
        _transaction?.Rollback();
        _transaction?.Dispose();
    }

    [BeforeFeature]
    public static void BeforeFeature()
    {
        // Note: BeforeFeature must be static; SpecFlow doesn't inject into static methods
        TestServer.Start();
    }

    [AfterFeature]
    public static void AfterFeature()
    {
        TestServer.Stop();
    }
}

Hooks can be ordered with the Order parameter: [BeforeScenario(Order = 1)]. Lower numbers run first for Before hooks and last for After hooks.

Hooks can also be scoped to tagged scenarios:

[BeforeScenario("database")]
public void SetupDatabase() { /* runs only for @database-tagged scenarios */ }

Table Parameters and SpecFlow.Assist

The SpecFlow.Assist helpers eliminate manual table parsing:

public class CartItem
{
    public string Product { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

[Given(@"my cart contains:")]
public void GivenMyCartContains(Table table)
{
    // CreateSet converts each table row into a CartItem
    var items = table.CreateSet<CartItem>().ToList();

    // CreateInstance converts a vertical two-column (Field | Value) table into an object
    // var item = table.CreateInstance<CartItem>();

    foreach (var item in items)
        _cartService.Add(item);
}

You can also use table.CompareToSet<T>() in Then steps to assert that a result collection matches a Gherkin table:

[Then(@"the order should contain:")]
public void ThenOrderShouldContain(Table table)
{
    var orderLines = _orderService.GetLines();
    table.CompareToSet<OrderLine>(orderLines); // throws on mismatch
}

Scenario Outlines

Scenario Outline: Discount tiers
  Given my cart total is <cart_total>
  When the discount engine applies
  Then the discount should be <expected_discount>%

  Examples:
    | cart_total | expected_discount |
    | 50.00      | 0                 |
    | 100.00     | 5                 |
    | 500.00     | 10                |
    | 1000.00    | 15                |

SpecFlow generates one test method per Examples row. The column names in <angle brackets> inside the step text are substituted at generation time.

Running Tests

# Run all SpecFlow tests with dotnet CLI
dotnet test

# Filter by tag
dotnet test --filter "TestCategory=smoke"

# NUnit-specific tag filter
dotnet test --filter "Category=integration"

# Generate TRX report
dotnet test --logger "trx;LogFileName=results.trx"

Map Gherkin tags to test categories in specflow.json:

{
  "generator": {
    "addNonParallelizableMarkerForTags": ["not-parallelizable"]
  }
}

Integration with HelpMeTest

SpecFlow scenarios describe the expected behavior of your system in business language. Once those scenarios are green in CI, the question becomes: does the deployed application actually behave this way in production?

HelpMeTest answers that question by running natural-language test scenarios against your live environment continuously — no C# required. A SpecFlow scenario like "When I proceed to checkout, Then my order should be confirmed" maps directly to a HelpMeTest monitor scenario that clicks through the real UI every hour.

The workflow: SpecFlow guards the code path during development, HelpMeTest monitors the deployed system 24/7. Both share the same Given/When/Then vocabulary, making it easy to translate business scenarios between layers.

Common Pitfalls

Missing step definitions cause generation errors, not runtime errors. SpecFlow generates C# test methods at build time. A step with no matching binding will produce a compile warning and a pending test, not a friendly runtime skip message.

Regex capture groups must match parameter count exactly. If your step pattern has two capture groups but the method has one parameter, SpecFlow throws BindingException at runtime. Always verify patterns in the IDE extension.

BeforeFeature / AfterFeature must be static. SpecFlow cannot inject instance dependencies into feature-scoped hooks because no scenario is active yet. Use the FeatureContext static accessor or design around this constraint.

ScenarioContext is scenario-scoped. Storing state in ScenarioContext rather than a dedicated context class works but couples your step definitions to the SpecFlow API. Prefer a plain C# context class injected via DI for easier unit testing of step logic.

Summary

SpecFlow makes BDD a first-class citizen in .NET projects. Feature files double as acceptance criteria that product owners can read; step bindings in C# give developers full access to the application stack. With hooks for lifecycle management, SpecFlow.Assist for typed table handling, and deep integration with NUnit/xUnit/MSTest, SpecFlow covers everything a .NET team needs to practice specification by example. Pair it with HelpMeTest to extend that same scenario coverage to your live production environment.

Read more

Start now free