BDD Testing with Reqnroll (Flawright.Reqnroll)
The Flawright.Reqnroll companion package lets you write Gherkin/BDD tests against Windows desktop applications using Reqnroll (the .NET successor to SpecFlow).
Quick start
1. Install the package
dotnet add package Flawright.Reqnroll
dotnet add package Reqnroll.xUnit # or Reqnroll.NUnit / Reqnroll.MsTest
dotnet add package Reqnroll.Tools.MsBuild.Generation
dotnet add package Microsoft.NET.Test.Sdk
2. Add reqnroll.json
Create reqnroll.json at your project root:
{
"$schema": "https://reqnroll.net/schemas/reqnroll-config-2.0.json",
"bindingCulture": { "name": "en-US" },
"trace": { "traceSuccessfulSteps": false },
"bindingAssemblies": [
{ "assembly": "Flawright.Reqnroll" }
]
}
The
bindingAssembliesentry tells Reqnroll to discover the 25 built-in step bindings from theFlawright.Reqnrollassembly. Without it, Reqnroll will not find any of the built-in steps and every scenario will fail with "step not found".
3. Write a feature file
@launch:notepad.exe
Feature: Notepad smoke test
Scenario: Type and verify text
Given I have the application in focus
When I fill "[name=\"Text editor\"]" with "Hello from Flawright!"
Then "[name=\"Text editor\"]" should contain "Hello"
4. Run
dotnet test
No bindings file needed — Flawright.Reqnroll ships 25 ready-to-use step bindings.
Tag reference
Tags on a feature or scenario tell Flawright.Reqnroll how to start (or attach to) the application for each scenario. One tag per scenario is usually enough.
| Tag | Effect | Example |
|---|---|---|
@launch:<path> |
Launch by executable path (Win32 or alias) | @launch:notepad.exe |
@aumid:<aumid> |
Launch a Store/UWP/WinUI3 app by AUMID | @aumid:Microsoft.WindowsCalculator_8wekyb3d8bbwe!App |
@attach:<name> |
Attach to a running process by name | @attach:notepad |
@attachpid:<pid> |
Attach to a running process by PID | @attachpid:12345 |
Tag precedence (highest to lowest): @attachpid: > @attach: > @aumid: > @launch: > FlawrightReqnrollOptions.DefaultAumid > FlawrightReqnrollOptions.DefaultApplicationPath.
Feature-level tags apply to every scenario in the feature. Scenario-level tags override feature-level ones.
Windows 11 Notepad note:
@launch:notepad.exeauto-resolves toLaunchStoreApp("Microsoft.WindowsNotepad_8wekyb3d8bbwe!App")on Windows 11 via Flawright's built-inAppExecutionAliasResolver. The Win11 editor selector is[name="Text editor"](notcontroltype:Edit).
Built-in step reference
Window focus
| Step | Regex |
|---|---|
Given I have the application in focus |
I have the application in focus |
When I bring the application to the front |
I bring the application to the front |
Mouse actions
| Step | Example |
|---|---|
When I click "<selector>" |
When I click "name:OK" |
When I double-click "<selector>" |
When I double-click "name:My Item" |
When I right-click "<selector>" |
When I right-click "name:My File" |
Text input
| Step | Example |
|---|---|
When I fill "<selector>" with "<value>" |
When I fill "#RichEditBox" with "Hello!" |
When I type "<text>" into "<selector>" |
When I type "hello" into "#RichEditBox" |
When I clear "<selector>" |
When I clear "#SearchBox" |
Keyboard
| Step | Example |
|---|---|
When I press "<key>" globally |
When I press "Ctrl+S" globally |
When I press "<key>" on "<selector>" |
When I press "Enter" on "#Submit" |
When I type "<text>" globally |
When I type "Hello World" globally |
Element state
| Step | Example |
|---|---|
When I focus "<selector>" |
When I focus "#SearchBox" |
When I hover over "<selector>" |
When I hover over "name:Help" |
Toggle / checkbox
| Step | Example |
|---|---|
When I check "<selector>" |
When I check "name:Dark Mode" |
When I uncheck "<selector>" |
When I uncheck "name:Notifications" |
Selection
| Step | Example |
|---|---|
When I select "<value>" from "<selector>" |
When I select "Dark" from "name:Theme" |
Wait
| Step | Example |
|---|---|
When I wait for <N> milliseconds |
When I wait for 500 milliseconds |
When I wait for selector "<selector>" |
When I wait for selector "name:Ready" |
Drag and drop
| Step | Example |
|---|---|
When I drag "<source>" to "<target>" |
When I drag "name:File" to "name:Folder" |
Assertions
| Step | Example |
|---|---|
Then "<selector>" should be visible |
Then "name:Save" should be visible |
Then "<selector>" should be hidden |
Then "name:Spinner" should be hidden |
Then "<selector>" should be enabled |
Then "name:Submit" should be enabled |
Then "<selector>" should be disabled |
Then "name:Submit" should be disabled |
Then "<selector>" should be checked |
Then "name:Remember Me" should be checked |
Then "<selector>" should contain "<text>" |
Then "#Editor" should contain "Hello" |
Then "<selector>" should have text "<text>" |
Then "name:Status" should have text "Ready" |
Then "<selector>" should have value "<value>" |
Then "#Amount" should have value "42" |
Then "<selector>" should be empty |
Then "#SearchBox" should be empty |
Then "<selector>" should have count <N> |
Then "controltype:ListItem" should have count 3 |
Then the window title should be "<title>" |
Then the window title should be "Notepad" |
Then the window title should contain "<text>" |
Then the window title should contain "Notepad" |
DI / sharing state between bindings
IFlawright, IFlawrightBrowser, and IFlawrightPage are registered into Reqnroll's BoDi container before any step runs. Inject them via constructor in your own [Binding] class:
using Flawright;
using Reqnroll;
[Binding]
public class MyCustomSteps
{
private readonly IFlawrightPage _page;
public MyCustomSteps(IFlawrightPage page)
{
_page = page;
}
[When(@"I save with keyboard shortcut")]
public async Task SaveAsync()
{
await _page.Keyboard.PressAsync("Ctrl+S");
}
}
The page is disposed automatically after each scenario — you never call DisposeAsync on it yourself.
Overriding built-in steps
Define a step with the same regex in your own [Binding] class. Reqnroll resolves the most specific match. To replace the built-in fill step with one that also logs:
[When(@"I fill ""([^""]*)"" with ""([^""]*)""")]
public async Task FillAndLogAsync(string selector, string value)
{
Console.WriteLine($"[FILL] {selector} <- {value}");
await _page.FillAsync(selector, value);
}
Global defaults with FlawrightReqnrollOptions
Register options once in a [BeforeTestRun] hook to set a default app path and timeout for the whole test run:
using Flawright;
using Flawright.Reqnroll;
using Reqnroll;
using Reqnroll.BoDi;
[Binding]
public static class TestRunHooks
{
[BeforeTestRun]
public static void Configure(IObjectContainer container)
{
container.RegisterInstanceAs(new FlawrightReqnrollOptions
{
DefaultApplicationPath = @"C:\MyApp\MyApp.exe",
FlawrightOptions = new FlawrightOptions
{
DefaultTimeout = TimeSpan.FromSeconds(15),
DefaultRetryInterval = TimeSpan.FromMilliseconds(50)
}
});
}
}
Any scenario without an explicit tag will then use DefaultApplicationPath as the launch target.
reqnroll.json — minimal config
{
"$schema": "https://reqnroll.net/schemas/reqnroll-config-2.0.json",
"bindingCulture": { "name": "en-US" },
"trace": { "traceSuccessfulSteps": false },
"bindingAssemblies": [
{ "assembly": "Flawright.Reqnroll" }
]
}
The bindingAssemblies entry is required. Without it Reqnroll only scans the test project assembly and will not find the built-in steps from Flawright.Reqnroll.
Limitations
- Interactive desktop session required. UI Automation only works in a real desktop session. On headless CI servers, a virtual display or Remote Desktop session is required. Use a Windows runner in GitHub Actions with
runs-on: windows-latest. - One scenario = one application instance. Each scenario gets a fresh app launch (for
@launch:/@aumid:) or attaches fresh (for@attach:/@attachpid:). Scenarios are not parallelised against the same app instance. - No xUnit parallel execution. Disable xUnit parallelism in your project by adding
[assembly: CollectionBehavior(DisableTestParallelization = true)]or settingparallelizeTestCollections: falseinxunit.runner.jsonto prevent multiple scenarios from fighting over the same desktop. - Win11 selector differences. Windows 11 ships WinUI3 rewrites of Notepad and Calculator. Their selectors differ from the classic Win32 versions. Always inspect with Accessibility Insights for Windows or FlaUI Inspect to get the correct selectors for your target system.