NUnit Testing with Selenium and C#: A Practical Tutorial
Build a C# browser test with NUnit and Selenium WebDriver, from project setup and driver management to reliable cleanup, waits, troubleshooting, and remote execution.
NUnit discovers and runs your C# tests, manages test lifecycle hooks, and provides assertions. Selenium WebDriver sends commands to a browser so your test can navigate pages, interact with elements, and inspect results. A minimal local test needs a .NET test project, NUnit, Selenium.WebDriver, a browser, and a test that always closes its driver.
This tutorial creates a project, installs the packages, and writes a test that submits text and checks the visible result. It also covers waits, browser options, driver setup, cleanup, remote execution, common failures, and a screenshot API option for cases where a full browser test is more than you need.
1. Understand NUnit and Selenium’s roles
Your C# test calls Selenium’s .NET binding. WebDriver communicates with the browser through its browser-specific driver. NUnit supplies test discovery, setup and teardown hooks, assertions, and test results. Selenium’s documentation emphasizes that WebDriver itself does not provide test assertions or pass/fail reporting: those belong to a test framework such as NUnit. Selenium documentation
For a local test, the browser usually runs on the same machine as the test process. With RemoteWebDriver or Selenium Grid, browser sessions can run on another machine. Start locally to learn the flow; use remote execution when you need different operating systems, browsers, or distributed capacity.
2. Create a C# NUnit project
Install the .NET SDK and create a project from the NUnit template:
dotnet new NUnit -n SeleniumNUnitTutorial
cd SeleniumNUnitTutorial
dotnet add package Selenium.WebDriver
The NUnit template includes the NUnit test framework and test SDK packages. If you are adding Selenium to an existing project, install compatible versions of NUnit, NUnit3TestAdapter, Microsoft.NET.Test.Sdk, and Selenium.WebDriver through NuGet. The exact package versions depend on your project’s target framework and dependency policy; check the current package listings and choose mutually compatible stable versions. Selenium’s install guide listed Selenium .NET 4.49.0 when this tutorial was researched on October 3, 2026. Selenium .NET install guide · Selenium.WebDriver on NuGet
Restore dependencies and run the starter tests:
dotnet restore
dotnet test
The Selenium documentation’s example test suite specifies .NET SDK 8.0 or later as its prerequisite. Treat that as the prerequisite for that documentation example, not as a universal minimum for every Selenium or NUnit project. Selenium first script
3. Write and run a complete browser test
Replace the starter test file with the following fixture. It opens a public demo page, enters a search term, submits the form, and asserts the result heading. The test uses an explicit wait for the result to appear and quits the browser during teardown even if an assertion fails.
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Support.UI;
namespace SeleniumNUnitTutorial;
[TestFixture]
public class SearchTests
{
private IWebDriver? _driver;
[SetUp]
public void SetUp()
{
_driver = new ChromeDriver();
}
[TearDown]
public void TearDown()
{
if (_driver is null)
{
return;
}
try
{
_driver.Quit();
}
finally
{
_driver.Dispose();
_driver = null;
}
}
[Test]
public void SearchShowsMatchingResult()
{
var driver = _driver ?? throw new InvalidOperationException("Browser was not initialized.");
driver.Navigate().GoToUrl("https://www.selenium.dev/selenium/web/web-form.html");
var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
var textBox = wait.Until(d => d.FindElement(By.Name("my-text")));
textBox.SendKeys("NUnit with Selenium");
driver.FindElement(By.TagName("button")).Click();
var heading = wait.Until(d =>
{
var element = d.FindElement(By.Id("message"));
return element.Displayed && !string.IsNullOrWhiteSpace(element.Text)
? element
: null;
});
Assert.That(heading.Text, Is.EqualTo("Received!"));
}
}
If your project does not have an implicit using for System, add using System; for InvalidOperationException and TimeSpan. The example’s page and locators come from Selenium’s documentation. Selenium first script
Run all tests with dotnet test. To run one test, filter by its fully qualified name, adjusting the namespace and class if you changed them:
dotnet test --filter "FullyQualifiedName=SeleniumNUnitTutorial.SearchTests.SearchShowsMatchingResult"
4. Manage browser startup and test lifecycle
Selenium Manager handles the usual driver setup
With current Selenium releases, new ChromeDriver() is often enough for local startup. Selenium Manager is bundled with Selenium and acts as a fallback when a driver has not already been provided. It can discover browser and driver versions, download the needed driver, and cache it. It is included with Selenium releases starting at 4.6. Manual driver provisioning remains useful in controlled CI environments or networks where automatic downloads are unavailable. Selenium Manager
Use per-test setup for isolation
[SetUp] runs before each test case and [TearDown] runs after each test case. A fresh browser per test helps prevent cookies, local storage, open tabs, and navigation state from leaking between tests. Use [OneTimeSetUp] and [OneTimeTearDown] only for fixture-wide resources that are safe to share. NUnit does not define the relative order of multiple setup methods in one class, so avoid depending on that order. NUnit SetUp · NUnit TearDown
Always end the browser session
Quit() ends the WebDriver session and closes its browser windows. Calling Dispose() releases the .NET object’s resources. Put cleanup in NUnit teardown rather than after the assertion: teardown still runs when the test fails. The try/finally around cleanup ensures disposal is attempted even if quitting throws.
5. Choose locators and waits that survive page changes
Prefer stable attributes such as IDs, names, or application-owned test attributes. Use CSS selectors when they clearly identify an element. Avoid brittle selectors tied to deep DOM structure or generated class names. A locator should represent the element’s purpose or a stable contract with the page.
Pages that update asynchronously need a wait for a condition, such as an element becoming visible or a result changing. The sample uses WebDriverWait with a ten-second maximum. Set the timeout to match the application and test environment, and wait for the state that proves the action succeeded. Fixed sleeps make every run wait the full duration and can still be too short under load. Avoid combining long implicit waits with explicit waits; that can make timeouts difficult to predict.
6. Configure Chrome for local and CI runs
Browser options let you set arguments, preferences, and capabilities before creating the driver. For example, a headless CI run can be configured like this:
var options = new ChromeOptions();
options.AddArgument("--headless=new");
options.AddArgument("--window-size=1440,1000");
_driver = new ChromeDriver(options);
Use headless mode only if it matches what you need to validate; headed and headless rendering can differ. Keep browser version and options consistent between local and CI environments when investigating a rendering issue. Do not put credentials or sensitive values in committed test code. Selenium’s browser-specific options and capabilities are documented in its WebDriver guides. Selenium browser options
7. Expand the test to other browsers or remote execution
To try another local browser, use its Selenium driver class and matching options package, for example FirefoxDriver or EdgeDriver, and install the corresponding browser. Driver discovery may be handled by Selenium Manager when possible.
When sessions should run elsewhere, create a RemoteWebDriver with the Grid endpoint and browser options:
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
var options = new ChromeOptions();
using IWebDriver driver = new RemoteWebDriver(
new Uri("http://localhost:4444"),
options);
driver.Navigate().GoToUrl("https://example.com");
This short example assumes a Selenium Grid is already available at that endpoint. Grid places browser sessions on remote machines and can help distribute execution across machines. Local execution has less infrastructure to operate; remote execution can expand browser and operating-system coverage and parallel capacity, with additional setup and infrastructure cost. The right choice depends on where browsers need to run and how many sessions your team needs. Selenium Grid documentation
8. Troubleshoot common failures
| Symptom | Likely cause | What to try |
|---|---|---|
| Driver or browser cannot start | The browser is missing, a driver cannot be resolved, or an environment blocks the download. | Confirm the browser is installed and reachable. Check network/proxy access for Selenium Manager, or provision a compatible driver explicitly in controlled environments. |
SessionNotCreatedException |
Browser and driver versions are incompatible, or the browser cannot launch with the requested options. | Update Selenium packages, verify the browser version, remove unsupported arguments, and inspect the full driver log. |
ElementNotFound or NoSuchElementException |
The locator is wrong, the page has not rendered the element, or the test navigated to an unexpected page. | Check the current URL and DOM, confirm the locator, and wait for the expected element state. |
ElementClickInterceptedException |
An overlay, animation, or sticky element covers the target. | Wait for the overlay to disappear or for the target to be clickable; verify the page state before clicking. |
| Wait times out intermittently | The condition is too strict, the timeout is too short for the environment, or the application did not reach the expected state. | Wait for a meaningful visible state, inspect browser/network logs, and fix slow or nondeterministic test data rather than adding arbitrary sleeps. |
| Tests pass alone but fail in the suite | Browser state, shared test data, or parallel access is leaking between cases. | Use per-test drivers, isolate test data, and avoid sharing mutable fixture state across parallel tests. |
| Browser process remains after a failure | Cleanup was skipped or the process could not be terminated cleanly. | Keep Quit() in teardown, dispose in a finally block, and inspect CI process cleanup if the host forcibly terminates tests. |
| Remote session cannot connect | Grid is unavailable, the URL is wrong, or the requested browser capability is not supported by the Grid nodes. | Check Grid health and endpoint, verify registered browser slots, and match options to the node configuration. |
9. Performance, reliability, and cost
- Startup: Browser creation is usually a significant part of a small test’s runtime. Reusing one browser per test suite can reduce startup work, but increases the chance of state leaking. Choose based on isolation needs.
- Waits: Condition-based waits reduce unnecessary idle time and make failures more informative than fixed delays.
- Parallelism: Parallel browser tests use more CPU and memory and need isolated drivers and test data. Start with a small parallel load and expand based on the capacity of the runner or Grid.
- Reliability: Stable locators, controlled test data, deterministic cleanup, and a known browser environment matter more than retries. Retries can hide flaky tests if the underlying cause is not fixed.
- Cost: Local Selenium uses your own development or CI machine resources. Grid or hosted browser infrastructure adds operational or service costs; this tutorial does not name a provider or quote pricing.
10. Or skip the browser setup
If the task is to capture a page image or PDF rather than test browser behavior, ScreenshotNeo is a website screenshot API and MCP server. A single GET request can return a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor, then removes 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. AI agents can use its MCP tools, including take_screenshot, get_page_info, and capture_pdf.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request parameters and response details. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. The MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, no card required.
11. Frequently asked questions
Does NUnit control the browser?
No. NUnit runs and reports tests. Selenium WebDriver controls the browser.
Do I need to download ChromeDriver manually?
Usually not with a current Selenium release and a supported local setup. Selenium Manager can resolve and cache a driver when one has not been supplied.
Should I use one browser for the whole fixture?
Use a fresh browser per test when isolation is important. Share a fixture-level browser only when you deliberately manage state and accept the coupling between tests.
When should I use Selenium Grid?
Use Grid when browsers need to run on remote machines, cover different environments, or execute across multiple nodes.
Can a screenshot API replace Selenium tests?
No. A screenshot API captures page output; it does not replace NUnit assertions or WebDriver interactions for testing application behavior.


