specflow-to-reqnroll

Installation
SKILL.md

SpecFlow → Reqnroll Migration

When to Use

  • A .csproj references SpecFlow, TechTalk.SpecFlow, SpecFlow.xUnit, SpecFlow.NUnit, SpecFlow.MsTest, or SpecFlow.Plus.LivingDocPlugin.
  • CI uses the stale SpecFlow.Plus.LivingDoc.CLI tool to produce LivingDoc.html.
  • The user wants to preserve Gherkin .feature files and existing step definitions while switching to an actively maintained BDD framework.
  • Running tests after a partial migration throws Reqnroll.BindingException: ... Alternative may not be empty.

Outcome

A compiling, runnable Reqnroll project that:

  • Keeps .feature files and step definitions virtually untouched.
  • Usually only needs namespace edits where TechTalk.SpecFlow was imported explicitly; projects using a global <Using Include="TechTalk.SpecFlow" /> often only need that global using switched to Reqnroll.
  • Emits an HTML living documentation report via Reqnroll's built-in formatter (no LivingDoc CLI).
  • Surfaces BDD test results and a living-doc download link in PRs; deploys gh-pages only on the default branch.

Procedure

Execute in order. Run dotnet build after each major step.

1. Inventory

  • Find every SpecFlow test project. For each, note:
    • Test framework: xUnit 2.x / xUnit v3 / NUnit / MsTest.
    • Custom Drivers, Hooks, and Step Definitions.
    • Presence of specflow.json and SpecFlow.Plus.LivingDocPlugin.

2. Update .csproj

Replace packages 1:1:

Old New
SpecFlow Reqnroll (>= 3.3.4)
SpecFlow.xUnit Reqnroll.xUnit (xUnit 2.x)
SpecFlow.xunit.v3 Reqnroll.xunit.v3
SpecFlow.NUnit Reqnroll.NUnit
SpecFlow.MsTest Reqnroll.MsTest
SpecFlow.Plus.LivingDocPlugin DELETE (replaced by built-in HTML formatter)

Update the global Using:

<ItemGroup>
    <Using Include="Reqnroll"/>
</ItemGroup>

Then replace explicit using TechTalk.SpecFlow; statements only in files that actually declare them (commonly hooks or custom infrastructure classes).

xUnit 2 runner fix: Reqnroll.xUnit 3.3.4 targets xUnit 2. Keep the runner on the xUnit 2 line as well. If the project references xunit.runner.visualstudio 3.x, downgrade it to 2.8.2. If restore also fails with NU1107 because xunit.core >= 2.8.1 is required, bump:

  • xunit2.9.3
  • xunit.runner.visualstudio2.8.2

3. Rename config & enable HTML formatter

specflow.jsonreqnroll.json. Add the formatters.html section to replace LivingDoc:

{
  "$schema": "https://schemas.reqnroll.net/reqnroll-config-latest.json",
  "bindingCulture": { "name": "zh-CN" },
  "language": { "feature": "en-US" },
  "formatters": {
    "html": { "outputFilePath": "reqnroll_report.html" }
  }
}

Keep or add stepAssemblies only when bindings live in another assembly. Do not add it by default for a normal single-project specs setup.

The HTML report appears in bin/<Config>/<tfm>/reqnroll_report.html after dotnet test.

4. Delete generated code-behind

rm Features/*.feature.cs

Reqnroll regenerates these during build with its own template. Leaving SpecFlow-generated files causes namespace/attribute conflicts.

5. Fix Cucumber Expression incompatibilities

Reqnroll's default step parser is Cucumber Expression (SpecFlow defaulted to regex). Key difference: / denotes "alternative branches". Any literal /, {, }, (, ) in step attributes that is not already part of a regex pattern like (.*) or ""(.*)"" must be escaped.

  • Wrong: [Given(@"url is /Account/Register")]
  • Right: [Given(@"url is \/Account\/Register")]

Attributes containing regex groups (e.g. (.*)) continue to work as regex and need no change.

Scan all [Given] / [When] / [Then] / [StepDefinition] attributes and fix unescaped separators.

6. Make Given steps idempotent

Reqnroll (like SpecFlow) does not guarantee scenario execution order. Any Given that mutates process-wide state (environment variables, static fields, singletons) must reset it, even if the scenario name says "is not set":

[Given(@"environment variable FOO is not set")]
public static void GivenFooIsNotSet()
{
    Environment.SetEnvironmentVariable("FOO", null);
}

Without this, a scenario that sets FOO=123 may run first and poison the "is not set" scenario.

7. Optimize self-hosted server driver (if present)

If the project has a Driver.cs that starts the system-under-test with dotnet run:

  • Change dotnet rundotnet run --no-build (skip redundant compilation).
  • Raise startup timeout from 60s to 120s.
  • In the wait loop, short-circuit if _serverProcess.HasExited becomes true — report the real crash instead of waiting for timeout.
  • Cache a local _serverStarted flag instead of HTTP-probing on every GetUrl call.

8. Update CI workflow

8.1 Replace LivingDoc generation — delete:

- name: Generate living documentation
  run: |
    dotnet tool install --global SpecFlow.Plus.LivingDoc.CLI
    livingdoc test-assembly path/to/Specs.dll -t path/to/TestExecution.json
    mv LivingDoc.html index.html

With:

- name: Generate living documentation
  run: cp tests/YourSpecs/bin/Debug/net9.0/reqnroll_report.html index.html

8.2 Publish BDD results to PR checks:

- name: BDD test
  run: |
    dotnet test tests/YourSpecs/YourSpecs.csproj \
      --logger "trx;LogFileName=bdd-results.trx" \
      --results-directory test-results

- name: Publish BDD test results to PR
  if: always() && github.event_name == 'pull_request'
  uses: dorny/test-reporter@v1
  with:
    name: BDD Test Results
    path: test-results/bdd-results.trx
    reporter: dotnet-trx
    fail-on-error: false   # MANDATORY: prevent double-red on failing tests

8.3 Living-doc artifact + sticky PR comment:

- name: Upload living doc artifact
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: living-doc
    path: index.html

- name: Comment living doc link on PR
  if: always() && github.event_name == 'pull_request'
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    header: living-doc
    message: |
      ## 📖 Living Documentation

      Download the `living-doc` artifact and open `index.html`.

      [Workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})

8.4 Gate gh-pages to default branch only. Job-level permissions on the living-doc job:

permissions:
  contents: read
  pull-requests: write
  checks: write

Guard Pages steps (replace develop with your default branch):

- name: Setup Pages
  if: github.ref == 'refs/heads/develop'
  uses: actions/configure-pages@v5

- name: Upload pages Artifact
  if: github.ref == 'refs/heads/develop'
  uses: actions/upload-pages-artifact@v3.0.1
  with:
    path: "."
    overwrite: true

Keep the same if on the downstream deploy-pages job.

9. Verify

  1. dotnet restore --force-evaluate to regenerate packages.lock.json.
  2. dotnet build0 errors (pre-existing warnings are not migration issues).
  3. Locally run at least one scenario to confirm Cucumber Expression fixes.
  4. Run the repository's existing dotnet test command as well, not only the migrated BDD project, to ensure the migration did not break the wider test suite.
  5. Push a PR and confirm:
    • ✅ "BDD Test Results" check appears with per-scenario status.
    • ✅ Sticky "Living Documentation" comment with artifact link.
    • ❌ No Pages artifact uploaded, no gh-pages deploy.
  6. Merge to default branch → gh-pages deploys with updated HTML report.

What NOT to change

  • .feature files (Gherkin is fully compatible).
  • [Binding], [Given], [When], [Then], [BeforeScenario], [AfterScenario], [StepArgumentTransformation] — all identical in Reqnroll.
  • Assertion libraries (FluentAssertions, xUnit asserts, etc.), Selenium / Playwright, HTTP clients.

Troubleshooting

Symptom Root cause Fix
Alternative may not be empty at column N Bare / in step attribute interpreted as Cucumber alternation Escape /\/
Timed out waiting for server to start dotnet run recompiling + missing dependency (e.g. PostgreSQL) Add --no-build; detect HasExited; ensure services running
Later scenario sees earlier scenario's state Given left state unchanged Reset env var / static field explicitly in Given
NU1107: xunit.core version conflict Reqnroll.xUnit requires newer xunit.core Bump xunit to 2.9.3+ and keep xunit.runner.visualstudio on 2.8.2 for xUnit 2 projects
Reqnroll migration "works" but runner package is still on xunit.runner.visualstudio 3.x Mixed xUnit 2/3 package line after swapping only the BDD package Downgrade xunit.runner.visualstudio to 2.8.2 when using Reqnroll.xUnit
test-reporter step itself fails on test failures fail-on-error: true default Set fail-on-error: false
PR triggers gh-pages deploy Pages steps / deploy job not gated Add if: github.ref == 'refs/heads/<default>'
Duplicate [Binding] classes / namespace errors Old .feature.cs left behind Delete Features/*.feature.cs

Related customizations to consider next

After completing this migration, the following reusable skills/instructions extracted from the same work are worth creating:

  1. gh-pages-living-doc skill — Generalize "publish an HTML test/documentation report to GitHub Pages, comment a download link on the PR, and gate the deploy to the default branch". Useful beyond Reqnroll (e.g. Playwright, Allure, Storybook reports).
  2. dotnet-test-pr-feedback instructions — Codify the mandatory combo for every .NET test job: dotnet test --logger "trx;LogFileName=*.trx" --results-directory test-results + dorny/test-reporter@v1 with fail-on-error: false + job permissions pull-requests: write / checks: write.
  3. bdd-scenario-isolation skill — Capture the scenario-order-independence pattern (reset env vars / static fields / DB rows in every Given, even for "is not set" cases), applicable to Reqnroll, SpecFlow, Cucumber-JVM, behave, etc.

References

Related skills
Installs
3
GitHub Stars
1
First Seen
Apr 21, 2026