NUnit and MSTest
Build robust C# Selenium tests using NUnit and MSTest frameworks.
Selenium 3 & 4 Stable
NUnit and MSTest are the primary testing frameworks for .NET/C# Selenium projects. Both integrate seamlessly with Visual Studio and provide powerful testing capabilities.
Framework Comparison
| Feature | NUnit | MSTest |
|---|---|---|
| Popularity | Most popular | Microsoft standard |
| Attributes | [Test], [TestCase] | [TestMethod], [DataRow] |
| Parallel | Built-in | Requires config |
| Assertions | Rich, fluent | Basic + FluentAssertions |
| IDE Support | Excellent | Native in VS |
NUnit Setup
Installation
# Via dotnet CLIdotnet add package NUnitdotnet add package NUnit3TestAdapterdotnet add package Selenium.WebDriverdotnet add package Selenium.Support
# Or via Package Manager ConsoleInstall-Package NUnitInstall-Package NUnit3TestAdapterInstall-Package Selenium.WebDriverProject Structure
SeleniumTests/├── SeleniumTests.csproj├── Pages/│ ├── BasePage.cs│ └── LoginPage.cs├── Tests/│ ├── BaseTest.cs│ ├── LoginTests.cs│ └── SearchTests.cs└── Utilities/ └── DriverFactory.csBasic NUnit Test
NUnit Test Structure
Selenium 3 & 4 Stable
// Java equivalent using TestNG (similar to NUnit)import org.testng.annotations.*;import org.testng.Assert;import org.openqa.selenium.*;import org.openqa.selenium.chrome.ChromeDriver;
public class LoginTests { private WebDriver driver;
@BeforeMethod public void setup() { driver = new ChromeDriver(); driver.manage().window().maximize(); }
@AfterMethod public void tearDown() { if (driver != null) { driver.quit(); } }
@Test public void testValidLogin() { driver.get("https://example.com/login");
driver.findElement(By.id("username")).sendKeys("testuser"); driver.findElement(By.id("password")).sendKeys("testpass"); driver.findElement(By.id("login-btn")).click();
Assert.assertTrue(driver.getCurrentUrl().contains("/dashboard")); }}# Python equivalent using pytestimport pytestfrom selenium import webdriverfrom selenium.webdriver.common.by import By
class TestLogin: def setup_method(self): self.driver = webdriver.Chrome() self.driver.maximize_window()
def teardown_method(self): self.driver.quit()
def test_valid_login(self): self.driver.get("https://example.com/login")
self.driver.find_element(By.ID, "username").send_keys("testuser") self.driver.find_element(By.ID, "password").send_keys("testpass") self.driver.find_element(By.ID, "login-btn").click()
assert "/dashboard" in self.driver.current_url// JavaScript equivalent using Jestdescribe('Login Tests', () => { let driver;
beforeEach(async () => { driver = await new Builder().forBrowser('chrome').build(); await driver.manage().window().maximize(); });
afterEach(async () => { await driver.quit(); });
test('valid login', async () => { await driver.get('https://example.com/login');
await driver.findElement(By.id('username')).sendKeys('testuser'); await driver.findElement(By.id('password')).sendKeys('testpass'); await driver.findElement(By.id('login-btn')).click();
const url = await driver.getCurrentUrl(); expect(url).toContain('/dashboard'); });});using NUnit.Framework;using OpenQA.Selenium;using OpenQA.Selenium.Chrome;
namespace SeleniumTests.Tests{ [TestFixture] public class LoginTests { private IWebDriver _driver;
[SetUp] public void Setup() { _driver = new ChromeDriver(); _driver.Manage().Window.Maximize(); _driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10); }
[TearDown] public void TearDown() { _driver?.Quit(); }
[Test] public void TestValidLogin() { _driver.Navigate().GoToUrl("https://example.com/login");
_driver.FindElement(By.Id("username")).SendKeys("testuser"); _driver.FindElement(By.Id("password")).SendKeys("testpass"); _driver.FindElement(By.Id("login-btn")).Click();
Assert.That(_driver.Url, Does.Contain("/dashboard")); }
[Test] public void TestInvalidLogin() { _driver.Navigate().GoToUrl("https://example.com/login");
_driver.FindElement(By.Id("username")).SendKeys("invalid"); _driver.FindElement(By.Id("password")).SendKeys("wrong"); _driver.FindElement(By.Id("login-btn")).Click();
var errorMessage = _driver.FindElement(By.ClassName("error")); Assert.That(errorMessage.Text, Does.Contain("Invalid credentials")); } }}MSTest Setup
Installation
# Via dotnet CLIdotnet add package MSTest.TestFrameworkdotnet add package MSTest.TestAdapterdotnet add package Selenium.WebDriverBasic MSTest Test
using Microsoft.VisualStudio.TestTools.UnitTesting;using OpenQA.Selenium;using OpenQA.Selenium.Chrome;
namespace SeleniumTests.Tests{ [TestClass] public class LoginTests { private IWebDriver _driver;
[TestInitialize] public void Setup() { _driver = new ChromeDriver(); _driver.Manage().Window.Maximize(); }
[TestCleanup] public void TearDown() { _driver?.Quit(); }
[TestMethod] public void TestValidLogin() { _driver.Navigate().GoToUrl("https://example.com/login");
_driver.FindElement(By.Id("username")).SendKeys("testuser"); _driver.FindElement(By.Id("password")).SendKeys("testpass"); _driver.FindElement(By.Id("login-btn")).Click();
Assert.IsTrue(_driver.Url.Contains("/dashboard")); } }}Data-Driven Tests
NUnit TestCase
[TestFixture]public class DataDrivenTests{ private IWebDriver _driver;
[SetUp] public void Setup() => _driver = new ChromeDriver();
[TearDown] public void TearDown() => _driver?.Quit();
[TestCase("testuser", "testpass", true)] [TestCase("admin", "admin123", true)] [TestCase("invalid", "wrong", false)] [TestCase("", "testpass", false)] public void TestLogin(string username, string password, bool shouldSucceed) { _driver.Navigate().GoToUrl("https://example.com/login");
_driver.FindElement(By.Id("username")).SendKeys(username); _driver.FindElement(By.Id("password")).SendKeys(password); _driver.FindElement(By.Id("login-btn")).Click();
if (shouldSucceed) { Assert.That(_driver.Url, Does.Contain("/dashboard")); } else { var error = _driver.FindElement(By.ClassName("error")); Assert.That(error.Displayed, Is.True); } }
// TestCaseSource for complex data private static IEnumerable<TestCaseData> LoginTestData() { yield return new TestCaseData("testuser", "testpass").Returns(true); yield return new TestCaseData("admin", "admin123").Returns(true); yield return new TestCaseData("invalid", "wrong").Returns(false); }
[Test, TestCaseSource(nameof(LoginTestData))] public bool TestLoginFromSource(string username, string password) { // Test implementation return true; // Simplified }}MSTest DataRow
[TestClass]public class DataDrivenTests{ private IWebDriver _driver;
[TestInitialize] public void Setup() => _driver = new ChromeDriver();
[TestCleanup] public void TearDown() => _driver?.Quit();
[DataTestMethod] [DataRow("testuser", "testpass", true)] [DataRow("admin", "admin123", true)] [DataRow("invalid", "wrong", false)] public void TestLogin(string username, string password, bool shouldSucceed) { _driver.Navigate().GoToUrl("https://example.com/login");
_driver.FindElement(By.Id("username")).SendKeys(username); _driver.FindElement(By.Id("password")).SendKeys(password); _driver.FindElement(By.Id("login-btn")).Click();
if (shouldSucceed) { Assert.IsTrue(_driver.Url.Contains("/dashboard")); } else { var error = _driver.FindElement(By.ClassName("error")); Assert.IsTrue(error.Displayed); } }}Base Test Class
using NUnit.Framework;using OpenQA.Selenium;using OpenQA.Selenium.Chrome;using OpenQA.Selenium.Support.UI;
namespace SeleniumTests.Tests{ public abstract class BaseTest { protected IWebDriver Driver { get; private set; } protected WebDriverWait Wait { get; private set; }
[SetUp] public virtual void Setup() { var options = new ChromeOptions();
// Headless mode from environment if (Environment.GetEnvironmentVariable("HEADLESS") == "true") { options.AddArgument("--headless=new"); }
Driver = new ChromeDriver(options); Driver.Manage().Window.Maximize(); Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(10)); }
[TearDown] public virtual void TearDown() { if (TestContext.CurrentContext.Result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed) { TakeScreenshot(); } Driver?.Quit(); }
protected void TakeScreenshot() { var screenshot = ((ITakesScreenshot)Driver).GetScreenshot(); var filename = $"screenshots/{TestContext.CurrentContext.Test.Name}.png"; Directory.CreateDirectory("screenshots"); screenshot.SaveAsFile(filename); TestContext.AddTestAttachment(filename); } }
// Tests extend BaseTest [TestFixture] public class LoginTests : BaseTest { [Test] public void TestValidLogin() { Driver.Navigate().GoToUrl("https://example.com/login"); // Uses inherited Driver and Wait } }}Parallel Execution
NUnit Parallel
// Assembly-level parallelism[assembly: Parallelizable(ParallelScope.Fixtures)][assembly: LevelOfParallelism(4)]
// Or per fixture[TestFixture][Parallelizable(ParallelScope.All)]public class ParallelTests{ // Each test runs in parallel // Must use separate driver per test!
[Test] public void Test1() { using var driver = new ChromeDriver(); // Test code }
[Test] public void Test2() { using var driver = new ChromeDriver(); // Test code }}MSTest Parallel
// In .runsettings file<?xml version="1.0" encoding="utf-8"?><RunSettings> <MSTest> <Parallelize> <Workers>4</Workers> <Scope>MethodLevel</Scope> </Parallelize> </MSTest></RunSettings>
// Or use assembly attribute[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]NUnit Assertions
// Classic assertionsAssert.AreEqual(expected, actual);Assert.IsTrue(condition);Assert.IsNotNull(obj);
// Constraint-based (recommended)Assert.That(actual, Is.EqualTo(expected));Assert.That(condition, Is.True);Assert.That(obj, Is.Not.Null);Assert.That(text, Does.Contain("substring"));Assert.That(text, Does.StartWith("Hello"));Assert.That(list, Has.Count.EqualTo(5));Assert.That(number, Is.InRange(1, 10));
// Multiple assertionsAssert.Multiple(() =>{ Assert.That(driver.Title, Does.Contain("Dashboard")); Assert.That(driver.Url, Does.Contain("/dashboard")); Assert.That(welcomeElement.Displayed, Is.True);});Categories and Filters
// NUnit categories[TestFixture][Category("Smoke")]public class SmokeTests{ [Test] [Category("Login")] public void TestBasicLogin() { }
[Test] [Category("Critical")] public void TestCriticalPath() { }}
// Run specific category// dotnet test --filter Category=Smoke// dotnet test --filter "Category=Smoke|Category=Critical"// MSTest categories[TestClass][TestCategory("Smoke")]public class SmokeTests{ [TestMethod] [TestCategory("Login")] public void TestBasicLogin() { }}
// dotnet test --filter TestCategory=SmokeRunning Tests
# Run all testsdotnet test
# Run specific projectdotnet test SeleniumTests.csproj
# Run with filterdotnet test --filter "FullyQualifiedName~LoginTests"dotnet test --filter Category=Smoke
# Run with verbositydotnet test -v detailed
# Generate reportdotnet test --logger "trx;LogFileName=results.trx"dotnet test --logger "html;LogFileName=results.html"Best Practices
| Practice | Description |
|---|---|
| Use constraint assertions | NUnit’s Assert.That is more readable |
| Implement base test class | Share setup/teardown logic |
| Screenshot on failure | Capture evidence for debugging |
| Use categories | Organize tests for selective running |
| Parallel with isolated drivers | Each test gets fresh driver |
Next Steps
- Page Object Model - Structure page classes
- Parallel Execution - Scale your tests
- Selenium Grid - Distributed testing