Skip to main content
SeleniumDecoded

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

FeatureNUnitMSTest
PopularityMost popularMicrosoft standard
Attributes[Test], [TestCase][TestMethod], [DataRow]
ParallelBuilt-inRequires config
AssertionsRich, fluentBasic + FluentAssertions
IDE SupportExcellentNative in VS

NUnit Setup

Installation

Terminal window
# Via dotnet CLI
dotnet add package NUnit
dotnet add package NUnit3TestAdapter
dotnet add package Selenium.WebDriver
dotnet add package Selenium.Support
# Or via Package Manager Console
Install-Package NUnit
Install-Package NUnit3TestAdapter
Install-Package Selenium.WebDriver

Project Structure

SeleniumTests/
├── SeleniumTests.csproj
├── Pages/
│ ├── BasePage.cs
│ └── LoginPage.cs
├── Tests/
│ ├── BaseTest.cs
│ ├── LoginTests.cs
│ └── SearchTests.cs
└── Utilities/
└── DriverFactory.cs

Basic 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 pytest
import pytest
from selenium import webdriver
from 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 Jest
describe('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

Terminal window
# Via dotnet CLI
dotnet add package MSTest.TestFramework
dotnet add package MSTest.TestAdapter
dotnet add package Selenium.WebDriver

Basic 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

Tests/BaseTest.cs
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 assertions
Assert.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 assertions
Assert.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=Smoke

Running Tests

Terminal window
# Run all tests
dotnet test
# Run specific project
dotnet test SeleniumTests.csproj
# Run with filter
dotnet test --filter "FullyQualifiedName~LoginTests"
dotnet test --filter Category=Smoke
# Run with verbosity
dotnet test -v detailed
# Generate report
dotnet test --logger "trx;LogFileName=results.trx"
dotnet test --logger "html;LogFileName=results.html"

Best Practices

PracticeDescription
Use constraint assertionsNUnit’s Assert.That is more readable
Implement base test classShare setup/teardown logic
Screenshot on failureCapture evidence for debugging
Use categoriesOrganize tests for selective running
Parallel with isolated driversEach test gets fresh driver

Next Steps