BDD with SpecFlow in C#: Gherkin, Step Definitions & Real-World Pitfalls
Most test suites are a black box that only developers can read. A QA engineer files a bug, a product manager writes acceptance criteria in Confluence, a developer writes a unit test β and none of these three artefacts ever talk to each other. Regressions slip through because the test suite tests what was built, not what was agreed. That gap between specification and verification is where production bugs are born.
Behaviour-Driven Development (BDD) closes that gap by making the specification executable. SpecFlow is the .NET implementation of Cucumber's BDD framework, and it lets you write plain-English feature files in Gherkin syntax that double as automated integration tests. Instead of translating requirements into xUnit tests manually β and inevitably losing nuance β you write the requirement once in a format every stakeholder can read, then bind it to C# code that actually exercises your system. When the scenario passes, the requirement is met. Full stop.
By the end of this article you'll know how to scaffold a SpecFlow project from scratch, write robust Gherkin scenarios including Scenario Outlines and Background, wire step definitions with regex and expression capture groups, manage shared state safely with dependency injection, hook into the test lifecycle with Before/After hooks, and sidestep the six most painful production gotchas β including the ambiguous step definition trap and the context injection anti-pattern that silently shares mutable state across parallel test runs.
Scaffolding a SpecFlow Project That Won't Embarrass You in Code Review
Before you write a single Gherkin line, you need a project structure that scales. The common mistake is adding SpecFlow to an existing xUnit or NUnit project and letting feature files pile up in the root. That works until you have 200 scenarios and no way to run a subset.
The recommended layout separates concerns cleanly: one project for your production code, one dedicated SpecFlow test project, and a shared contracts project if multiple test projects need the same interfaces. SpecFlow 3.9+ targets .NET 6/7/8 and ships as a set of NuGet packages β the runner (SpecFlow.xUnit, SpecFlow.NUnit, or SpecFlow.MsTest), the core (SpecFlow), and the code generator (SpecFlow.Tools.MsBuild.Generation).
The code generator is the secret engine. At build time it reads every .feature file and generates a .feature.cs file alongside it β a partial class that contains a standard xUnit/NUnit test method per scenario. You never edit that generated file. Your step definitions live in separate classes. This separation means the Gherkin stays human-readable while the plumbing stays out of sight.
Install SpecFlow.xUnit and SpecFlow.Tools.MsBuild.Generation together. Missing the MsBuild package is mistake #1 β your feature files silently produce no tests and the test runner reports zero discoveries.
# ββ Terminal: scaffold the solution ββββββββββββββββββββββββββββββββββββββββββ # 1. Create a blank solution dotnet new sln -n VendingMachine # 2. Production code project dotnet new classlib -n VendingMachine.Core -o src/VendingMachine.Core dotnet sln add src/VendingMachine.Core/VendingMachine.Core.csproj # 3. SpecFlow test project (xUnit runner) dotnet new xunit -n VendingMachine.Specs -o tests/VendingMachine.Specs dotnet sln add tests/VendingMachine.Specs/VendingMachine.Specs.csproj # 4. Reference production code from the test project dotnet add tests/VendingMachine.Specs reference src/VendingMachine.Core # 5. Add SpecFlow NuGet packages dotnet add tests/VendingMachine.Specs package SpecFlow dotnet add tests/VendingMachine.Specs package SpecFlow.xUnit dotnet add tests/VendingMachine.Specs package SpecFlow.Tools.MsBuild.Generation # ββ tests/VendingMachine.Specs/VendingMachine.Specs.csproj ββββββββββββββββββββ # After adding packages, verify this appears in the .csproj: # # <ItemGroup> # <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" /> # </ItemGroup> # # SpecFlow also needs this property to generate .feature.cs files: # <GenerateAssemblyInfo>false</GenerateAssemblyInfo> β only if you hit duplicate attribute errors # 6. Build β the MsBuild generator runs here and produces *.feature.cs files dotnet build # ββ Folder layout after setup βββββββββββββββββββββββββββββββββββββββββββββββββ # VendingMachine/ # βββ src/ # β βββ VendingMachine.Core/ β production code # β βββ VendingMachineService.cs # βββ tests/ # βββ VendingMachine.Specs/ # βββ Features/ β .feature files live here # β βββ Dispensing.feature # βββ StepDefinitions/ β C# step binding classes # β βββ DispensingSteps.cs # βββ Support/ β hooks, context objects, DI setup # βββ Hooks.cs # βββ VendingMachineContext.cs
VendingMachine.Core -> src/VendingMachine.Core/bin/Debug/net8.0/VendingMachine.Core.dll
VendingMachine.Specs -> tests/VendingMachine.Specs/bin/Debug/net8.0/VendingMachine.Specs.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Gherkin Deep Dive: Scenarios, Outlines, and Background That Actually Model Reality
Gherkin has six keywords you'll use daily: Feature, Background, Scenario, Scenario Outline, Examples, and the step keywords Given/When/Then/And/But. The novice writes one Scenario per happy path and calls it done. The senior uses the full toolkit to model edge cases without duplicating prose.
Background runs its steps before every Scenario in the file. Use it for setup that truly applies to every single scenario β like pre-loading a product catalogue. Don't abuse it as a dumping ground for unrelated setup, or scenarios become impossible to understand in isolation.
Scenario Outline is the data-driven workhorse. You write one scenario template with angle-bracket placeholders and provide an Examples table. SpecFlow generates a separate test method per row. This is far cleaner than a loop inside a step definition because each row is a first-class, individually-named test β failures point to the exact row.
The And and But keywords inherit the keyword of the preceding step for readability. Under the hood SpecFlow treats them identically. The distinction is purely for human readers β it reads like a sentence, not a list.
One deep detail: Gherkin scenarios should describe observable behaviour from the outside, not implementation steps. 'When I call the CalculateChange() method' is a bad scenario. 'When I insert $2.00 into a machine priced at $1.50' is a good scenario. The test should survive a complete internal rewrite.
# tests/VendingMachine.Specs/Features/Dispensing.feature Feature: Vending Machine Dispensing As a thirsty customer I want to insert money and select a product So that I receive my drink and the correct change # Background runs before EVERY scenario in this file. # Use it only for setup that genuinely applies to all scenarios. Background: Given the vending machine is stocked with the following products | ProductCode | Name | PriceInCents | Quantity | | A1 | Cola | 150 | 5 | | B2 | Water | 100 | 3 | | C3 | OrangeJuice | 200 | 0 | # ββ Happy path ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ Scenario: Customer receives product and exact change Given the customer inserts 200 cents When the customer selects product "A1" Then the customer receives "Cola" And the customer receives 50 cents change And the machine stock for "A1" decreases by 1 # ββ Out-of-stock guard βββββββββββββββββββββββββββββββββββββββββββββββββββββ Scenario: Customer selects an out-of-stock product Given the customer inserts 200 cents When the customer selects product "C3" Then the customer receives an out-of-stock error And the customer receives 200 cents change # ββ Insufficient funds ββββββββββββββββββββββββββββββββββββββββββββββββββββ Scenario: Customer inserts too little money Given the customer inserts 50 cents When the customer selects product "A1" Then the customer receives an insufficient funds error But the machine retains 50 cents # ββ Data-driven with Scenario Outline βββββββββββββββββββββββββββββββββββββ # Each row in Examples becomes a separate, independently-named test method. # Placeholders in <angle brackets> are substituted per row. Scenario Outline: Change calculation across multiple price points Given the customer inserts <InsertedCents> cents When the customer selects product "<ProductCode>" Then the customer receives "<ExpectedProduct>" And the customer receives <ExpectedChange> cents change Examples: | InsertedCents | ProductCode | ExpectedProduct | ExpectedChange | | 150 | A1 | Cola | 0 | | 200 | A1 | Cola | 50 | | 200 | B2 | Water | 100 | | 300 | B2 | Water | 200 |
# After dotnet build, SpecFlow generates Dispensing.feature.cs in the obj/ folder.
# dotnet test will discover and list:
#
# VendingMachine.Specs
# Dispensing - Vending Machine Dispensing
# Customer receives product and exact change [PASS]
# Customer selects an out-of-stock product [PASS]
# Customer inserts too little money [PASS]
# Change calculation across multiple price points (150,A1,Cola,0) [PASS]
# Change calculation across multiple price points (200,A1,Cola,50) [PASS]
# Change calculation across multiple price points (200,B2,Water,100) [PASS]
# Change calculation across multiple price points (300,B2,Water,200) [PASS]
Step Definitions, Regex Capture, and Shared State with Context Injection
A step definition is a C# method decorated with [Given], [When], or [Then] and a pattern string. When SpecFlow's step binder sees a Gherkin step, it regex-matches it against every registered pattern and invokes the winner. Understanding this matching engine is what separates a SpecFlow beginner from someone who can maintain a 1,000-scenario suite.
SpecFlow supports two pattern styles: regular expressions and SpecFlow Expressions (formerly Cucumber Expressions). SpecFlow Expressions use curly-brace type hints like {int} and {string} and are usually cleaner. Under the hood they still compile to regex. Use regex directly only when you need look-aheads, optional groups, or other constructs the expression syntax doesn't cover.
Shared state is the hardest problem. Scenarios have multiple steps, and those steps often need to share objects β the machine instance, the dispensing result, the error that was thrown. Global static fields are a disaster in parallel runs. SpecFlow's answer is ScenarioContext and context injection. Prefer context injection: declare a plain C# class as your shared container, accept it in multiple step definition constructors, and SpecFlow's built-in BoDi IoC container creates one instance per scenario and injects it wherever needed. It's disposed after the scenario ends β clean every time.
Hooks ([BeforeScenario], [AfterScenario], [BeforeFeature] etc.) let you plug into the lifecycle without polluting step definitions with setup logic. They're the right place to spin up an in-memory database, start a WireMock server, or reset a singleton between scenarios.
// ββ src/VendingMachine.Core/VendingMachineService.cs βββββββββββββββββββββββββ namespace VendingMachine.Core; public record Product(string Code, string Name, int PriceInCents, int Quantity); public class DispensingResult { public Product? DispensedProduct { get; init; } public int ChangeInCents { get; init; } public string? ErrorMessage { get; init; } public bool Success => ErrorMessage is null; } public class VendingMachineService { // Mutable stock dictionary β keyed by product code private readonly Dictionary<string, Product> _stock; private int _insertedCents; public VendingMachineService(IEnumerable<Product> initialStock) { _stock = initialStock.ToDictionary(p => p.Code); } public void InsertMoney(int cents) => _insertedCents += cents; public DispensingResult SelectProduct(string productCode) { if (!_stock.TryGetValue(productCode, out var product)) return new DispensingResult { ErrorMessage = "UNKNOWN_PRODUCT", ChangeInCents = _insertedCents }; if (product.Quantity == 0) { // Return all money β machine never keeps money for out-of-stock int refund = _insertedCents; _insertedCents = 0; return new DispensingResult { ErrorMessage = "OUT_OF_STOCK", ChangeInCents = refund }; } if (_insertedCents < product.PriceInCents) return new DispensingResult { ErrorMessage = "INSUFFICIENT_FUNDS" }; int change = _insertedCents - product.PriceInCents; _insertedCents = 0; // Decrement stock β records are immutable so we replace the entry _stock[productCode] = product with { Quantity = product.Quantity - 1 }; return new DispensingResult { DispensedProduct = product, ChangeInCents = change }; } public int GetStockQuantity(string productCode) => _stock.TryGetValue(productCode, out var p) ? p.Quantity : 0; public int RetainedCents => _insertedCents; } // ββ tests/VendingMachine.Specs/Support/VendingMachineContext.cs βββββββββββββββ // This is the shared-state bag injected across step definition classes. // One instance per scenario β created and disposed by SpecFlow's BoDi container. namespace VendingMachine.Specs.Support; using VendingMachine.Core; public class VendingMachineContext { // The system under test β populated by Background steps public VendingMachineService? Machine { get; set; } // The result of the most recent SelectProduct call public DispensingResult? LastResult { get; set; } // Track initial quantities so we can assert decrements public Dictionary<string, int> InitialQuantities { get; } = new(); } // ββ tests/VendingMachine.Specs/StepDefinitions/DispensingSteps.cs βββββββββββββ namespace VendingMachine.Specs.StepDefinitions; using TechTalk.SpecFlow; using TechTalk.SpecFlow.Assist; // for table.CreateSet<T>() using VendingMachine.Core; using VendingMachine.Specs.Support; using Xunit; // [Binding] tells SpecFlow to scan this class for step definitions and hooks. [Binding] public sealed class DispensingSteps { // Context injected by BoDi β same instance shared with any other step class // that also accepts VendingMachineContext in its constructor this scenario. private readonly VendingMachineContext _ctx; public DispensingSteps(VendingMachineContext ctx) { _ctx = ctx; } // ββ Background step ββββββββββββββββββββββββββββββββββββββββββββββββββββββ // Table.CreateSet<T>() uses the column headers as property names (case-insensitive). // It handles int conversion automatically via the Assist library. [Given(@"the vending machine is stocked with the following products")] public void GivenTheMachineIsStockedWithProducts(Table stockTable) { var products = stockTable.CreateSet<Product>(); // maps rows β Product records _ctx.Machine = new VendingMachineService(products); // Snapshot initial quantities so assertions can check decrements foreach (var product in products) _ctx.InitialQuantities[product.Code] = product.Quantity; } // ββ Given steps ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // {int} is a SpecFlow Expression β matches one or more digits, auto-converts to int. [Given(@"the customer inserts {int} cents")] public void GivenCustomerInsertsMoney(int cents) { _ctx.Machine!.InsertMoney(cents); } // ββ When steps βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // {string} matches a double-quoted string in Gherkin and strips the quotes. [When(@"the customer selects product {string}")] public void WhenCustomerSelectsProduct(string productCode) { // Store result on context so Then steps can assert against it _ctx.LastResult = _ctx.Machine!.SelectProduct(productCode); } // ββ Then steps βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ [Then(@"the customer receives {string}")] public void ThenCustomerReceivesProduct(string expectedProductName) { Assert.NotNull(_ctx.LastResult); Assert.True(_ctx.LastResult.Success, $"Expected success but got error: {_ctx.LastResult.ErrorMessage}"); Assert.Equal(expectedProductName, _ctx.LastResult.DispensedProduct?.Name); } [Then(@"the customer receives {int} cents change")] public void ThenCustomerReceivesChange(int expectedChangeInCents) { Assert.Equal(expectedChangeInCents, _ctx.LastResult!.ChangeInCents); } [Then(@"the customer receives an out-of-stock error")] public void ThenCustomerReceivesOutOfStockError() { Assert.Equal("OUT_OF_STOCK", _ctx.LastResult!.ErrorMessage); } [Then(@"the customer receives an insufficient funds error")] public void ThenCustomerReceivesInsufficientFundsError() { Assert.Equal("INSUFFICIENT_FUNDS", _ctx.LastResult!.ErrorMessage); } // "But" steps use [Then] β SpecFlow treats But identically to Then at runtime. [Then(@"the machine retains {int} cents")] public void ThenMachineRetainsMoney(int expectedRetainedCents) { Assert.Equal(expectedRetainedCents, _ctx.Machine!.RetainedCents); } [Then(@"the machine stock for {string} decreases by {int}")] public void ThenStockDecreases(string productCode, int decrementAmount) { int initial = _ctx.InitialQuantities[productCode]; int current = _ctx.Machine!.GetStockQuantity(productCode); Assert.Equal(decrementAmount, initial - current); } } // ββ tests/VendingMachine.Specs/Support/Hooks.cs βββββββββββββββββββββββββββββββ // Hooks that run outside the step definitions β clean lifecycle management. namespace VendingMachine.Specs.Support; using TechTalk.SpecFlow; [Binding] public sealed class Hooks { // [BeforeScenario] runs before every scenario. // If you need to reset external state (e.g. a real DB), do it here. // The VendingMachineContext is already fresh per-scenario via BoDi, // so we only need this hook if we have truly external resources. [BeforeScenario] public void LogScenarioStart(ScenarioContext scenarioContext) { Console.WriteLine($"[START] {scenarioContext.ScenarioInfo.Title}"); } [AfterScenario] public void LogScenarioResult(ScenarioContext scenarioContext) { var status = scenarioContext.TestError is null ? "PASSED" : "FAILED"; Console.WriteLine($"[{status}] {scenarioContext.ScenarioInfo.Title}"); // In a real suite you'd flush logs, take screenshots, or reset DB here. } }
[START] Customer receives product and exact change
[PASSED] Customer receives product and exact change
[START] Customer selects an out-of-stock product
[PASSED] Customer selects an out-of-stock product
[START] Customer inserts too little money
[PASSED] Customer inserts too little money
[START] Change calculation across multiple price points
[PASSED] Change calculation across multiple price points
[START] Change calculation across multiple price points
[PASSED] Change calculation across multiple price points
[START] Change calculation across multiple price points
[PASSED] Change calculation across multiple price points
[START] Change calculation across multiple price points
[PASSED] Change calculation across multiple price points
Passed! - Failed: 0, Passed: 7, Skipped: 0, Total: 7
Parallel Execution, Tags, and Production-Grade Configuration
By default SpecFlow with xUnit runs scenarios in parallel at the feature level β one feature file per thread. That sounds great until you realise your step definitions share a static HttpClient or a database connection string that's mutated per-scenario. Parallelism exposes every shared-mutable-state bug you haven't found yet.
The safest model: mark your test assembly with [assembly: CollectionBehavior(CollectionBehavior.CollectionPerClass)] to control xUnit's parallelism, then let SpecFlow manage scenario-level isolation via context injection. Never use static fields in step definition or context classes. If you need a database, spin up a separate schema per scenario using a GUID suffix on the schema name.
Tags let you slice the test suite. A Scenario tagged @smoke runs in your fast pre-deploy pipeline. A Scenario tagged @slow runs nightly. You can filter from the CLI: dotnet test --filter 'Category=smoke'. Tags also let Hooks fire selectively β [BeforeScenario("smoke")] only runs for @smoke-tagged scenarios. This is how you attach WireMock or a real API key only where needed.
The specflow.json configuration file controls runtime behaviour β output plugin, step assembly scanning, and (crucially) whether missing steps are treated as Pending or Inconclusive. Set missingOrPendingStepsOutcome to Error in production pipelines so an unimplemented step fails the build immediately rather than being silently skipped.
// ββ tests/VendingMachine.Specs/specflow.json ββββββββββββββββββββββββββββββββββ // This file controls SpecFlow runtime behaviour. // Place it in the test project root alongside the .csproj. /* { "$schema": "https://specflow.org/specflow-default.json", "language": { "feature": "en" }, "bindingCulture": { "name": "en-US" }, "runtime": { "missingOrPendingStepsOutcome": "Error" } } */ // β missingOrPendingStepsOutcome: "Error" means an unbound step breaks the build. // Change to "Pending" locally during active development so you can run partial suites. // ββ tests/VendingMachine.Specs/AssemblyInfo.cs ββββββββββββββββββββββββββββββββ // Controls xUnit's parallelism β must sit at assembly level. using Xunit; // CollectionPerAssembly: all test classes share one thread unless you opt individual // classes into a named Collection. Use CollectionPerClass for full scenario isolation. [assembly: CollectionBehavior(CollectionBehavior.CollectionPerClass, DisableTestParallelization = false)] // ββ Feature file with tags ββββββββββββββββββββββββββββββββββββββββββββββββββββ // Dispensing.feature (tag examples shown inline as comments) // // @smoke β applies to all scenarios below until next tag group // Scenario: Customer receives product and exact change // ... // // @slow @integration // Scenario: Customer sees stock sync with warehouse API // ... // ββ CLI: run only @smoke scenarios βββββββββββββββββββββββββββββββββββββββββββ // dotnet test tests/VendingMachine.Specs --filter 'Category=smoke' // ββ tests/VendingMachine.Specs/Support/SmokeHooks.cs βββββββββββββββββββββββββ // Hook that fires ONLY for scenarios tagged @smoke namespace VendingMachine.Specs.Support; using TechTalk.SpecFlow; [Binding] public sealed class SmokeHooks { private readonly ScenarioContext _scenarioContext; public SmokeHooks(ScenarioContext scenarioContext) { _scenarioContext = scenarioContext; } // The string argument to [BeforeScenario] is a tag filter. // This method only executes when the current scenario carries @smoke. [BeforeScenario("smoke")] public void ConfigureSmokeEnvironment() { Console.WriteLine($"Smoke setup for: {_scenarioContext.ScenarioInfo.Title}"); // e.g. set a lighter timeout, skip WireMock, use in-memory repos } // ββ Parallel isolation demo βββββββββββββββββββββββββββββββββββββββββββββββ // BAD β static field will cause race conditions in parallel runs: // private static VendingMachineService? _sharedMachine; // β NEVER DO THIS // GOOD β injected context is per-scenario, thread-safe: // private readonly VendingMachineContext _ctx; // β injected via constructor } // ββ Advanced: registering custom services in BoDi ββββββββββββββββββββββββββββ // If you need interfaces (e.g. IProductRepository), register them in a BeforeScenario hook. [Binding] public sealed class DependencyRegistration { private readonly IObjectContainer _container; // BoDi's IoC container public DependencyRegistration(IObjectContainer container) { _container = container; } [BeforeScenario] public void RegisterScenarioDependencies() { // Register a fake repository for every scenario. // BoDi will inject IProductRepository wherever it's requested in step classes. // Swap for a real implementation in @integration-tagged scenarios. _container.RegisterTypeAs<InMemoryProductRepository, IProductRepository>(); } } // Marker interface and stub implementation β just enough to show the pattern public interface IProductRepository { /* ... */ } public class InMemoryProductRepository : IProductRepository { /* ... */ }
Test run for tests/VendingMachine.Specs/bin/Debug/net8.0/VendingMachine.Specs.dll (.NETCoreApp,Version=v8.0)
Microsoft (R) Test Execution Command Line Tool Version 17.8.0
Smoke setup for: Customer receives product and exact change
[START] Customer receives product and exact change
[PASSED] Customer receives product and exact change
Passed! - Failed: 0, Passed: 1, Skipped: 6, Total: 7
# Only @smoke-tagged scenario ran. The other 6 were skipped.
| Aspect | ScenarioContext (Dictionary) | Context Injection (BoDi) |
|---|---|---|
| Thread Safety | Thread-static β breaks in parallel runs | Per-scenario scoped β safe in all parallelism modes |
| Type Safety | Object casting required β runtime errors | Strongly typed β compile-time checked |
| Discoverability | Magic string keys β invisible dependencies | Constructor parameters β dependencies are explicit |
| Testability | Cannot easily unit-test step setup logic | Context class is a POCO β trivially unit-testable |
| Disposal | Manual cleanup in AfterScenario hook required | BoDi calls IDisposable.Dispose() automatically |
| Setup Complexity | Zero config β works out of the box | Needs [BeforeScenario] for non-POCO registrations |
| Best For | Quick prototypes, legacy migrations | Any production SpecFlow suite |
π― Key Takeaways
- Always add SpecFlow.Tools.MsBuild.Generation β without it your feature files produce zero tests and the runner gives you no error, just silence.
- Use context injection via BoDi constructor injection for all shared scenario state β ScenarioContext.Current is thread-static and silently breaks in parallel runs.
- Gherkin scenarios should describe observable user behaviour, not method calls β a scenario that survives an internal refactor is worth ten that don't.
- missingOrPendingStepsOutcome: Error in specflow.json is non-negotiable in CI pipelines β it converts a silent pending step into a build-breaking failure.
β Common Mistakes to Avoid
- βMistake 1: Writing implementation-level Gherkin steps β 'When I call CalculateChange() with 200 and 150' β makes scenarios brittle to refactors and meaningless to non-developers. Fix: describe observable, user-visible behaviour β 'When I insert $2.00 and select a $1.50 cola'. The test survives a complete internal redesign and remains readable by a product manager.
- βMistake 2: Sharing mutable state via static fields in step definition classes β in parallel test runs two scenarios overwrite each other's data causing random, unreproducible failures. Fix: move all per-scenario state into a context class and inject it via constructor. SpecFlow's BoDi container creates a fresh instance per scenario and disposes it after β zero manual cleanup needed.
- βMistake 3: Forgetting to add SpecFlow.Tools.MsBuild.Generation β feature files exist on disk but generate zero test methods, and dotnet test reports 'No tests found' with no diagnostic error. Fix: add the NuGet package, rebuild, then verify .feature.cs files appear inside the obj/ folder. If they don't appear after a clean rebuild, check that the feature file's Build Action is set to None (not Compile) in the .csproj.
Interview Questions on This Topic
- QWhat is the difference between ScenarioContext and context injection in SpecFlow, and why does it matter in a parallel test run?
- QHow does SpecFlow's MsBuild code generator work, and what happens at runtime when a Gherkin step has no matching step definition?
- QYou have 500 SpecFlow scenarios and the nightly build takes 45 minutes. Walk me through the strategies you'd use to bring it under 10 minutes without removing scenarios.
Frequently Asked Questions
What is the difference between SpecFlow and NUnit or xUnit?
xUnit and NUnit are test runners β they execute C# test methods and report pass/fail. SpecFlow sits on top of them and adds a Gherkin parsing layer that translates plain-English feature files into those test methods automatically. SpecFlow needs a runner (xUnit, NUnit, or MSTest) underneath it; it doesn't replace them.
Can SpecFlow scenarios run in parallel, and is it safe?
Yes, SpecFlow with xUnit supports parallel execution at the feature level by default. It's safe as long as you use context injection instead of static shared state β each scenario gets its own injected context instance. If your scenarios touch an external database, isolate them with per-scenario schemas or transactions that roll back in an AfterScenario hook.
What is the difference between a Scenario and a Scenario Outline in SpecFlow?
A Scenario is a single concrete test case with fixed values. A Scenario Outline is a template with angle-bracket placeholders and an Examples table β SpecFlow generates one independent test method per row. Use Scenario Outline whenever you want to verify the same behaviour across multiple input/output combinations without duplicating the prose.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful β not just SEO filler.