In Playwright for .NET, a NUnit insurance-quote suite needs cross-browser runs, retries and traces -- what must the team build itself?
answer
- The config file was doing four jobs
- One CI job per engine
- Tracing is your setup and teardown
- Retries belong to the host runner
- Shared base-class state must be thread-safe
basics
~20 sEverything the JavaScript config would declare. Playwright for .NET has no projects, retries or trace mode, so the browser comes from run settings or a CI job per engine, tracing from explicit Context.Tracing calls, and retries from the host runner.
solid answer
~40 sThe .NET packages give you the `PageTest` family of base classes and run-settings parameters, and nothing else. Cross-browser coverage becomes a run-level parameter -- typically one CI job per engine invoking `dotnet test -- Playwright.BrowserName=firefox` -- because `projects` is a JavaScript-runner concept. Traces are manual: start `Context.Tracing` in setup and stop it with a path in teardown, ideally in one shared base class that derives from `PageTest` and only keeps the zip when the test failed. Retries, parallel execution and result files come from the host runner, not from Playwright. The main hazard is that host-runner parallelism is thread-based inside one process, so any state your base class shares must be concurrency-safe. What the team does not have to rebuild is the library itself: locators, auto-waiting, `page.route()` and tracing are all present in .NET.
code
csharp · 33 linesusing System.IO;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
public class QuoteTests : PageTest
{
[SetUp]
public async Task StartTracing() =>
await Context.Tracing.StartAsync(new()
{
Screenshots = true,
Snapshots = true,
Sources = true
});
[TearDown]
public async Task StopTracing() =>
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine("traces", $"{TestContext.CurrentContext.Test.Name}.zip")
});
[Test]
public async Task AnnualPremiumIsShown()
{
await Page.GotoAsync("https://quotes.example.com/motor");
await Page.GetByLabel("Vehicle value").FillAsync("18500");
await Page.GetByRole(AriaRole.Button, new() { Name = "Get quote" }).ClickAsync();
await Expect(Page.GetByTestId("annual-premium")).ToHaveTextAsync("412.60");
}
}go deeper
Take away the headline: the .NET packages give base classes and run settings, not a config file. Anything you saw declared in a JavaScript Playwright config becomes code or CI configuration in a .NET suite.
Explain each replacement concretely: browser name as a run-settings parameter, tracing started and stopped on Context in setup and teardown, retries and parallelism taken from the host runner rather than from Playwright.
Demonstrate the operating judgment: one CI job per engine, traces kept only for failures, artefacts published per job, and shared base-class state audited for thread safety before parallelism is switched on.
Frame the cost honestly. About a day of harness work replaces the config file, most of it CI orchestration your organisation already owns, and the irreplaceable parts, the library features, are fully present in .NET.
This is the moment a modernisation project meets the library-versus-runner split head on. The team read the Playwright documentation, saw a config file that sets up three browsers, two retries and an HTML report in fifteen lines, and then discovered that none of it applies because their suite is C# on NUnit. Everything on that list still happens; it just becomes their code and their CI configuration. ## What is missing, and where it used to live - a `projects` matrix -- the JavaScript runner's way of running the same tests on several engines with different options - `retries`, including the automatic trace-on-first-retry policy that usually rides along with it - the worker model, `--shard`, and the HTML report that aggregates a sharded run - `test.extend` fixtures and `test.use`, so per-file overrides are hooks and base classes instead - `expect(page).toHaveScreenshot()` and component mounting The .NET packages give exactly two things: the `PlaywrightTest` through `PageTest` base classes, and run-settings parameters read from `dotnet test` or a `.runsettings` file. ## Building a browser matrix There is no per-project configuration, so the engine becomes a run-level parameter. Two shapes work, and the choice is a CI question rather than a code one: 1. One CI job per engine, each invoking `dotnet test -- Playwright.BrowserName=chromium` and so on. Simple, gives independent results and independent artefacts, and is the usual answer for an insurance-quote regression pack that runs nightly. 2. Parametrised inside the suite, deriving from `PlaywrightTest` and building the context yourself so the engine can vary per test case. More flexible, but you have given up the `PageTest` convenience that made the migration attractive. Either way the per-project options a JavaScript config carries -- a different base URL, a different storage state, a dependency on a setup project -- have to be expressed in run settings or in your own base class. ## Getting traces The .NET binding has no trace mode setting. You start and stop tracing yourself on the context the base class gave you: - `await Context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true })` in setup - `await Context.Tracing.StopAsync(new() { Path = ... })` in teardown, with a path derived from the test name Write that once in a shared base class deriving from `PageTest`, and inspect the test's outcome in teardown if you only want traces for failures -- that conditional is the manual equivalent of a retain-on-failure policy. The zip it produces is the ordinary Playwright trace format. ## Retries, parallelism and reporting These are not Playwright's to give in .NET, and the honest answer names their real owner: | Concern | Where it comes from in a .NET suite | | --- | --- | | retries | the host runner's own retry facility, configured in the suite | | parallelism | the host runner's parallel execution settings | | reporting | the host runner's result files, consumed by CI | | shard splitting | CI-level test filtering across jobs | The trap is that the host runner's parallelism is thread-based within one process, while a JavaScript run uses separate worker processes. Any state your base class holds -- a shared context, a static `IPlaywright`, a cached login -- must therefore be safe for concurrent tests. This is the single most common source of flakiness when a suite that ran serially is suddenly parallelised. ## A working shape for the suite A pragmatic modernisation ends up with roughly this: 1. A shared base class deriving from `PageTest` that starts and stops tracing and applies the base URL. 2. Run settings checked into the repository, with the browser name overridden per CI job. 3. One CI job per engine, each publishing the runner's result file and the trace directory as artefacts. 4. Retries configured conservatively at the host-runner level, with a rule that a retried test is a defect to investigate rather than a green build. ## What to tell the team Say plainly that Playwright's automation library is fully available in .NET and its runner is not, so roughly a day of harness work replaces the config file. Then push back on scope: much of what the JavaScript config does is CI orchestration, and the team already owns a CI system that can do it. The genuinely irreplaceable parts are the library features -- auto-waiting, locators, routing, tracing -- and those they have.
- How would you keep traces only for failing tests, the way a retain-on-failure policy would?Stop tracing in teardown either way, but decide the path from the test's outcome: write the zip when the result is a failure and discard it otherwise. Putting that in one shared base class keeps the conditional in a single place, and keeps a full nightly run from producing gigabytes of traces nobody opens.
- What breaks first when this suite is switched from serial to parallel execution?Shared state in the base class or in static fields. Host-runner parallelism is thread-based inside one process, so a cached login, a static Playwright instance or a shared context is now touched by several tests at once. Each test needs its own context, which the PageTest base class already gives it, and any other shared object must be immutable or per-test.
saying these in an interview costs you the question
- Expects playwright.config.ts to configure a .NET run
- Assumes PageTest records a trace automatically
- Says .NET cannot produce traces at all
- Plans to keep a single shared context across parallel tests
- Treats a passing retry as a green result worth ignoring