スポンサーリンク

Reqnroll + Playwright Project Structure in .NET (Best Practices)

This article is part of the “Introduction to E2E Testing with Reqnroll × Playwright” series.

Reqnroll + Playwright Project Structure in .NET (Best Practices)

Conclusion

The best way to use Playwright in a Reqnroll (.NET) project is to separate responsibilities into Hooks, Steps, Pages, and Features.
This structure keeps your E2E tests clean, scalable, and easy to maintain, especially as your test suite grows.

What You’ll Learn

In this guide, you’ll learn:

  • The recommended project structure for Reqnroll + Playwright
  • Where to initialize Playwright
  • How Step Definitions interact with the browser
  • How to manage the Page object
  • Why the Page Object pattern improves maintainability

This article focuses on code structure, not environment setup.

Recommended Project Structure

A common structure for Reqnroll + Playwright projects looks like this:

TestProject
├ Hooks
│ └ PlaywrightHooks.cs

├ Pages
│ └ LoginPage.cs

├ Steps
│ └ LoginSteps.cs

└ Features
└ Login.feature

Folder Responsibilities

FolderPurpose
FeaturesBDD scenarios (Gherkin)
StepsScenario implementation (C#)
HooksPlaywright setup and teardown
PagesPage Object classes

This separation makes your test code easier to manage as it scales.

Initializing Playwright with Hooks

The recommended place to initialize Playwright is inside Hooks.

Reqnroll provides lifecycle hooks like:

  • BeforeScenario
  • AfterScenario

These allow you to manage setup and cleanup in one place.

Example: PlaywrightHooks

using Microsoft.Playwright;
using Reqnroll;

[Binding]
public class PlaywrightHooks
{
public static IPage Page;
private static IBrowser browser;
private static IPlaywright playwright;

[BeforeScenario]
public async Task BeforeScenario()
{
playwright = await Playwright.CreateAsync();

browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = false
});

var context = await browser.NewContextAsync();
Page = await context.NewPageAsync();
}

[AfterScenario]
public async Task AfterScenario()
{
await browser.CloseAsync();
playwright.Dispose();
}
}

PlaywrightHooks Class Explained

Role of the PlaywrightHooks Class

This class uses Reqnroll’s Hooks feature to start and stop the Playwright browser before and after test execution.

In Reqnroll, Hooks allow you to centralize common setup and teardown logic, such as:

  • Pre-test processing
  • Post-test processing

In this implementation, a browser is launched for each Scenario and closed when the Scenario finishes.

Using Directives

using Microsoft.Playwright;
using Reqnroll;

These statements import the required libraries.

NamespacePurpose
Microsoft.PlaywrightPlaywright API
ReqnrollReqnroll BDD functionality

Classes used to control Playwright (such as IPage and IBrowser) are included in the Microsoft.Playwright namespace.

Binding Attribute

[Binding]
public class PlaywrightHooks

The [Binding] attribute tells Reqnroll to recognize this class during test execution.

Any class marked with [Binding] is registered as:

  • Step Definitions
  • Hooks

This attribute is required for the class to function as Hooks.

Field (Variable) Definitions

public static IPage Page;
private static IBrowser browser;
private static IPlaywright playwright;

These fields define the main Playwright objects.

VariableRole
IPlaywrightPlaywright core
IBrowserBrowser instance
IPageBrowser tab (page)

Playwright operates with the following hierarchy:

Playwright

Browser

BrowserContext

Page

In this setup, the Page object is exposed so it can be used in Step Definitions.

It is declared as public static so it can be accessed from other classes.

Example:

await PlaywrightHooks.Page.GotoAsync("https://example.com");

BeforeScenario

[BeforeScenario]
public async Task BeforeScenario()

[BeforeScenario] marks a method that runs before each Scenario.

Execution flow:

Scenario Start

BeforeScenario

Step Definitions

Starting Playwright

playwright = await Playwright.CreateAsync();

This creates a Playwright instance.

You must create this object before using Playwright.


Launching the Browser

browser = await playwright.Chromium.LaunchAsync(
new BrowserTypeLaunchOptions
{
Headless = false
});

This launches a Chromium browser.

The Headless setting controls whether the browser UI is displayed:

ValueMeaning
trueRuns without UI (headless)
falseShows the browser window

For debugging, setting Headless = false is useful to observe browser behavior.

Creating a BrowserContext

var context = await browser.NewContextAsync();

A BrowserContext represents an isolated browser session.

Conceptually:

Browser
├ Session 1 (incognito)
├ Session 2 (incognito)

By creating a new context per Scenario, you can isolate:

  • Cookies
  • LocalStorage
  • Session data

Creating a Page

Page = await context.NewPageAsync();

This creates a browser tab (Page).

All Playwright interactions are performed through this object.

For example:

Page.GotoAsync()
Page.ClickAsync()
Page.FillAsync()

AfterScenario

[AfterScenario]
public async Task AfterScenario()

[AfterScenario] runs after each Scenario completes.

Execution flow:

Scenario End

AfterScenario

Closing the Browser

await browser.CloseAsync();

This closes the browser.

Closing the browser after each test helps:

  • Prevent memory leaks
  • Ensure test isolation

Disposing Playwright

playwright.Dispose();

This releases Playwright resources.

In .NET, it is recommended to call Dispose when working with libraries that use unmanaged or external resources.


Overall Execution Flow

The Hooks execute in the following order:

Scenario Start

BeforeScenario

Playwright initialized

Browser launched

Context created

Page created

Step Definitions executed

AfterScenario

Browser closed

Playwright disposed

Using Playwright in Step Definitions

Step Definitions use the Page created in Hooks.

Example

using Reqnroll;

[Binding]
public class LoginSteps
{
[Given(@"I open the login page")]
public async Task OpenLoginPage()
{
await PlaywrightHooks.Page.GotoAsync("https://example.com/login");
}

[When(@"I enter username and password")]
public async Task InputLogin()
{
await PlaywrightHooks.Page.FillAsync("#user", "test");
await PlaywrightHooks.Page.FillAsync("#password", "password");
}

[When(@"I click the login button")]
public async Task ClickLogin()
{
await PlaywrightHooks.Page.ClickAsync("#login");
}
}

Execution Flow

Feature → Step Definition → Playwright → Browser

Using the Page Object Pattern

As your test code grows, writing Playwright logic directly in Step Definitions becomes hard to maintain.

The solution is the Page Object pattern.

Example: LoginPage

using Microsoft.Playwright;

public class LoginPage
{
private readonly IPage page;

public LoginPage(IPage page)
{
this.page = page;
}

public async Task Open()
{
await page.GotoAsync("https://example.com/login");
}

public async Task Login(string user, string password)
{
await page.FillAsync("#user", user);
await page.FillAsync("#password", password);
await page.ClickAsync("#login");
}
}

Using Page Object in Step Definitions

[Binding]
public class LoginSteps
{
private readonly LoginPage loginPage;

public LoginSteps()
{
loginPage = new LoginPage(PlaywrightHooks.Page);
}

[Given(@"I open the login page")]
public async Task OpenPage()
{
await loginPage.Open();
}
}

Benefits

  • Cleaner Step Definitions
  • Reusable UI logic
  • Easier maintenance when UI changes

Overall Test Architecture

A well-structured test follows this flow:

Feature (Gherkin)

Step Definition (C#)

Page Object

Playwright

Browser Automation

This layered approach improves readability and scalability.

Best Practices

Keep Responsibilities Separate

  • Hooks → setup/teardown
  • Steps → test logic
  • Pages → UI interactions

Use One Page Object per Screen

Avoid mixing multiple pages in one class.

Isolate Scenarios

Create a new BrowserContext per scenario to avoid shared state.

Use Headed Mode for Debugging

Set:

Headless = false

to visually debug browser behavior.

Summary

To build maintainable E2E tests with Reqnroll and Playwright in .NET:

  • Use Hooks to manage browser lifecycle
  • Use Step Definitions for scenario logic
  • Use Page Objects for UI operations
  • Follow a clear folder structure

This approach ensures your test code remains clean, scalable, and production-ready as your project grows.