specflow-to-reqnroll
SpecFlow → Reqnroll Migration
When to Use
- A
.csprojreferencesSpecFlow,TechTalk.SpecFlow,SpecFlow.xUnit,SpecFlow.NUnit,SpecFlow.MsTest, orSpecFlow.Plus.LivingDocPlugin. - CI uses the stale
SpecFlow.Plus.LivingDoc.CLItool to produceLivingDoc.html. - The user wants to preserve Gherkin
.featurefiles 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
.featurefiles and step definitions virtually untouched. - Usually only needs namespace edits where
TechTalk.SpecFlowwas imported explicitly; projects using a global<Using Include="TechTalk.SpecFlow" />often only need that global using switched toReqnroll. - 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.jsonandSpecFlow.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:
xunit→2.9.3xunit.runner.visualstudio→2.8.2
3. Rename config & enable HTML formatter
specflow.json → reqnroll.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 run→dotnet run --no-build(skip redundant compilation). - Raise startup timeout from 60s to 120s.
- In the wait loop, short-circuit if
_serverProcess.HasExitedbecomes true — report the real crash instead of waiting for timeout. - Cache a local
_serverStartedflag instead of HTTP-probing on everyGetUrlcall.
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
dotnet restore --force-evaluateto regeneratepackages.lock.json.dotnet build→ 0 errors (pre-existing warnings are not migration issues).- Locally run at least one scenario to confirm Cucumber Expression fixes.
- Run the repository's existing
dotnet testcommand as well, not only the migrated BDD project, to ensure the migration did not break the wider test suite. - 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.
- Merge to default branch → gh-pages deploys with updated HTML report.
What NOT to change
.featurefiles (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:
gh-pages-living-docskill — 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).dotnet-test-pr-feedbackinstructions — Codify the mandatory combo for every .NET test job:dotnet test --logger "trx;LogFileName=*.trx" --results-directory test-results+dorny/test-reporter@v1withfail-on-error: false+ job permissionspull-requests: write/checks: write.bdd-scenario-isolationskill — 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
More from jeff-tian/agent-skills
oidc-integration
Plan and implement OIDC and OAuth 2.0 integration for React or TypeScript frontends and Java or Spring Boot backends. Use whenever the user mentions OIDC, OpenID Connect, OAuth login, SSO, PKCE, authorization code flow, refresh tokens, JWT or JWKS validation, login callback pages, protected routes, Keycloak, Auth0, IdentityServer, Authing, multi-provider auth, or "add login" and "integrate IdP" style requests even if they do not explicitly say OIDC.
9tdd
Use when implementing any feature or bugfix, before writing implementation code
6cohosted-frontend-backend
Co-host a frontend SPA and backend API in the same server process or container — separate codebases in development, single entry point in production. Use when integrating React/Next.js/Umi frontends with Node.js (Fastify/Egg.js) or ASP.NET Core backends. Trigger on keywords like "co-hosted frontend backend", "single container deployment", "SPA + API same server", "add frontend to backend project", "integrate Next.js with .NET", "static export", "SSR takeover", etc.
4