ScreenshotNeo

BlogHow-to

How to Write Parameterized Selenium Tests with xUnit

Run the same Selenium check against multiple inputs with xUnit theories. Set up a C# test project, isolate browser state, run each case, and diagnose failures.

By the ScreenshotNeo team4 October 20268 min read

Use an xUnit [Theory] when a Selenium check should run for multiple inputs. Put each input row in [InlineData]; xUnit reports each row as its own test case. Create and dispose a WebDriver for each invocation so one row’s browser state cannot affect another.

This tutorial uses xUnit.net v2, dotnet test with the VSTest runner, Selenium.WebDriver, and Chrome. The commands below pin xUnit to the documented v2.9.3 example line. Install a current Selenium.WebDriver package compatible with your target .NET SDK, and check your project template’s runner settings before copying commands: xUnit v2 is in maintenance mode, while v3 has different templates and runner choices. See the official xUnit v2 guide, xUnit v3 guide, and v3 runner guide.

1. Create a test project

Install the .NET SDK supported by your environment. The official xUnit v2 guide’s example uses SDK 9.0.301 and targets .NET 8; its exact versions are examples, not a universal SDK requirement. Create a project and add the test framework, adapter, and Selenium packages:

dotnet new xunit -n SeleniumParameterizedTests -f net8.0
cd SeleniumParameterizedTests
dotnet add package xunit --version 2.9.3
dotnet add package xunit.runner.visualstudio --version 3.0.1
dotnet add package Microsoft.NET.Test.Sdk --version 17.12.0
dotnet add package Selenium.WebDriver

Package versions shown for the runner and test SDK are explicit example pins. Confirm that they fit the SDK and project template you select; keep the test SDK, xUnit framework and runner adapter compatible. Selenium’s .NET first-script documentation uses .NET SDK 8.0 or later for its example test suite, which is not a universal minimum for all Selenium projects. See Selenium’s first script documentation.

For an existing project, inspect its .csproj and retain its chosen runner configuration. The VSTest project below uses dotnet test. xUnit v3 can use Microsoft Testing Platform (MTP) or VSTest, with configuration and commands depending on that choice; do not mix instructions for the two.

2. Write a theory with independent browser cases

This complete example opens a stable public page and verifies its page title for two URL inputs. Replace the URLs and expected titles with pages your application owns or a stable test environment. Each [InlineData] row supplies the method arguments for one separately reported case.

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using Xunit;

namespace SeleniumParameterizedTests;

public class PageTitleTests
{
    [Theory]
    [InlineData("https://example.com/", "Example Domain")]
    [InlineData("https://www.iana.org/domains/reserved", "IANA-managed Reserved Domains")]
    public void Page_has_expected_title(string url, string expectedTitle)
    {
        // One driver per theory invocation keeps cookies, tabs, and navigation isolated.
        using var driver = new ChromeDriver();
        driver.Navigate().GoToUrl(url);

        Assert.Equal(expectedTitle, driver.Title);
    }
}

Save this as PageTitleTests.cs and run dotnet test from the project directory. Chrome must be installed and usable in the environment. Selenium Manager, included with Selenium, can manage the browser driver in common setups; restricted or offline build agents may need a browser and driver installed and configured explicitly. Consult the official Selenium WebDriver documentation for setup details.

Why use a theory?

A [Fact] tests an invariant condition with no supplied data. A [Theory] tests behavior that depends on supplied data. xUnit’s [InlineData] associates literal argument rows with the theory method. The number, order, and types of each row’s values must match the method parameters. xUnit shows row values with the case result, making a failing input easier to identify.

Use representative inputs

Choose rows that exercise distinct behavior, such as a normal URL, a URL with a query string, or a known redirect. Avoid using an unstable external website for a test that gates a deployment: content, network access, consent screens, and availability can change outside your control. Prefer a controlled test page with known expected results.

If the rows become lengthy, generated, or need setup, move them into a separate data source such as xUnit’s member data APIs after confirming syntax for your xUnit version. Keep test inputs understandable in runner output. Do not silently build expected values from the same logic being tested.

3. Run cases and read failures

For this VSTest-compatible xUnit v2 project, run:

dotnet test

Run a specific test project from a solution with dotnet test path/to/SeleniumParameterizedTests.csproj. Use the test runner configured by the project template. In xUnit v3 with MTP, follow its project configuration and invocation instructions; the command behavior may differ from a VSTest project.

When a theory row fails, inspect the reported arguments first. For example, if only the IANA row fails, check that page’s current title, redirects, network availability, and whether the browser reached the expected page. A failed assertion means the observed title differed from the expected string; a driver or navigation exception means the browser did not reach the assertion normally.

4. Keep browser state isolated

Creating the driver inside the theory method gives every row a fresh browser session and disposes it at the end, including when an assertion fails. This is a straightforward isolation pattern, though starting a browser for every row has a startup cost.

A shared fixture can reduce browser startup overhead, but it changes the isolation boundary. Cookies, local storage, open tabs, and navigation can leak between invocations if a shared driver is reused. If you choose a fixture, explicitly reset browser state before each case and arrange cleanup in the lifecycle supported by your xUnit version. Consider parallel execution: tests that share a mutable driver must not run concurrently. Per-invocation drivers avoid that shared-driver race, but browser and machine resource limits still constrain how many browsers can run at once.

5. Use screenshot evidence when it helps

A title assertion is often enough for a small navigation check. For visual debugging or page review, capture the rendered page after it reaches the state under test. A screenshot can help explain a layout or loading failure, but it does not replace assertions on the behavior that matters.

Or skip the browser setup

If the task is capturing a website image or PDF rather than exercising browser interactions, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF; see the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month with no card.

Performance, reliability, and cost

  • Startup time: a fresh browser per row costs more startup time than a carefully reset shared browser. Favor isolation for correctness; use a shared lifecycle only when its state-reset behavior is explicit.
  • Parallel runs: independent drivers can run cases concurrently, but each browser consumes resources. On constrained agents, cap test parallelism through the runner/project configuration rather than sharing a mutable driver across concurrent cases.
  • Reliable inputs: external pages can change, redirect, or become unavailable. A controlled test site and deterministic expected results reduce unrelated failures. Keep network-dependent checks separate from tests that must pass offline.
  • Cleanup: dispose drivers even when assertions fail. The using statement does that for the per-case example.
  • Cost: Selenium itself is open-source software, but browser execution consumes CI or machine time. A screenshot service may have its own usage pricing; ScreenshotNeo’s published plans include 1,000 free monthly shots, then paid plans from $5 for 3,000.

Troubleshooting

Symptom Likely cause Fix
dotnet test discovers no tests The test SDK or xUnit runner adapter is missing or incompatible, or the project uses a different runner configuration. Check package references and the generated project template. For v2 with VSTest, include the test SDK and Visual Studio runner adapter. For v3, follow the selected MTP or VSTest setup.
Chrome driver or browser startup fails Chrome is absent, the environment blocks driver downloads, or the browser and driver setup is incompatible. Install a supported browser and configure the driver for the agent, or allow the setup mechanism to obtain it. Check Selenium’s driver setup documentation and environment logs.
One data row fails with a title mismatch The URL redirected, the page changed, or the expected title is wrong for that row. Read the row arguments in the failure output, inspect the actual destination and title, and update the controlled fixture or expectation.
Navigation times out or the browser cannot resolve a host The test environment has no route to the site, DNS/TLS trouble, or a slow external dependency. Use a reachable test endpoint, check agent network access, and set a deliberate page-load timeout where needed. Avoid arbitrary long waits as a substitute for controlling the page.
Rows pass alone but fail in a suite A reused driver or fixture leaks cookies, storage, tabs, or navigation state; parallel tests may also race on shared state. Create a driver per invocation or reset all relevant state and prevent concurrent use of a shared driver.
Tests pass locally but fail in CI Headless/display configuration, browser installation, permissions, network access, or parallel resource pressure differs. Make browser setup explicit in CI, inspect driver logs, and reduce concurrency if the agent is resource constrained.

Frequently asked questions

Can I pass multiple URLs to one xUnit test?

Yes. Add one [InlineData] row per URL and include any expected result as another method argument. xUnit runs and reports each row as an individual theory case.

How do I run each Selenium theory case separately?

They are individual cases in the test runner already. To select one case, use the filtering syntax supported by your configured runner and identify the theory row by its displayed arguments; exact filters vary by runner and version.

Should every row get its own WebDriver?

It is the simplest way to isolate browser state. A shared fixture can save startup time, but requires explicit cleanup and safe handling of parallel execution.

Should this project use xUnit v2 or v3?

For an existing v2 project, use its established packages and runner. For a new project, check the current v3 template and runner documentation, then pin compatible versions. The official v2 guide describes v2 as maintenance mode.