.NET UI Testing
Introduction
User Interface (UI) testing is a critical component of the software testing process that ensures your application's interface behaves as expected when users interact with it. In the .NET ecosystem, several tools and frameworks are available to help developers create robust UI tests that can validate everything from simple button clicks to complex user workflows.
UI testing differs from unit or integration testing because it focuses on testing the application from the user's perspective, interacting with the visible elements of your application rather than testing individual components or methods in isolation. This approach helps catch issues that might not be evident in lower-level tests, such as layout problems, navigation errors, or functionality that doesn't work correctly when integrated into the UI.
Why UI Testing Matters
Before diving into the technical implementation, let's understand why UI testing is crucial:
- User Experience Validation: Ensures the application behaves correctly from the user's perspective
- Cross-browser Compatibility: Verifies functionality works consistently across different browsers
- Regression Prevention: Catches interface issues before they reach production
- Workflow Validation: Tests complete user journeys through the application
Popular .NET UI Testing Tools
1. Selenium WebDriver
Selenium is one of the most widely used tools for browser automation and UI testing. In the .NET world, you can use Selenium WebDriver with C# to create powerful UI tests.
Setting Up Selenium with .NET
First, install the necessary NuGet packages:
dotnet add package Selenium.WebDriver
dotnet add package Selenium.Support
dotnet add package NUnit # or your preferred test framework
Basic Selenium Test Example
Here's a simple test that navigates to a website and verifies its title:
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
namespace WebUITests
{
public class BasicSeleniumTest
{
private IWebDriver _driver;
[SetUp]
public void Setup()
{
// Initialize the Chrome browser
_driver = new ChromeDriver();
}
[Test]
public void TestWebsiteTitle()
{
// Navigate to the website
_driver.Navigate().GoToUrl("https://www.example.com");
// Assert that the title contains the expected text
Assert.That(_driver.Title, Contains.Substring("Example Domain"));
// Optional: Take a screenshot
((ITakesScreenshot)_driver).GetScreenshot().SaveAsFile("homepage.png");
}
[TearDown]
public void TearDown()
{
// Close the browser
_driver?.Quit();
}
}
}
Testing Form Submission with Selenium
Here's a more complex example that demonstrates filling out and submitting a form:
[Test]
public void TestFormSubmission()
{
// Navigate to the form page
_driver.Navigate().GoToUrl("https://www.example.com/form");
// Find and fill form elements
_driver.FindElement(By.Id("name")).SendKeys("John Doe");
_driver.FindElement(By.Id("email")).SendKeys("[email protected]");
// Select from a dropdown
var selectElement = new SelectElement(_driver.FindElement(By.Id("country")));
selectElement.SelectByText("United States");
// Check a checkbox
_driver.FindElement(By.Id("newsletter")).Click();
// Submit the form
_driver.FindElement(By.Id("submit-button")).Click();
// Wait for the success message
var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
var successMessage = wait.Until(driver =>
driver.FindElement(By.Id("confirmation-message")));
// Assert the confirmation message appears
Assert.That(successMessage.Text, Is.EqualTo("Thank you for your submission!"));
}
2. Playwright for .NET
Playwright is a newer UI testing tool that addresses many of the limitations of Selenium. It offers built-in auto-waiting, better browser isolation, and faster execution.
Setting Up Playwright
First, install the NuGet package:
dotnet add package Microsoft.Playwright
dotnet add package Microsoft.Playwright.NUnit # if using NUnit
After installing, you'll need to install the required browsers:
pwsh bin/Debug/net6.0/playwright.ps1 install
Basic Playwright Test Example
using Microsoft.Playwright;
using NUnit.Framework;
using System.Threading.Tasks;
namespace PlaywrightTests
{
public class BasicPlaywrightTest
{
[Test]
public async Task NavigateAndCheckTitle()
{
// Initialize Playwright
using var playwright = await Playwright.CreateAsync();
// Launch a browser (Chrome/Chromium in this case)
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
{
Headless = false // Set to true in CI environments
});
// Create a new context (similar to an incognito window)
var context = await browser.NewContextAsync();
// Create a new page
var page = await context.NewPageAsync();
// Navigate to the website
await page.GotoAsync("https://www.example.com");
// Get the title and verify it
var title = await page.TitleAsync();
Assert.That(title, Is.EqualTo("Example Domain"));
// Take a screenshot
await page.ScreenshotAsync(new PageScreenshotOptions { Path = "example.png" });
}
}
}
Advanced Playwright Features
Playwright offers many advanced features that make UI testing more reliable:
[Test]
public async Task TestFormInteraction()
{
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync();
var context = await browser.NewContextAsync();
var page = await context.NewPageAsync();
// Navigate to the form page
await page.GotoAsync("https://www.example.com/form");
// Fill the form - Playwright has a convenient Fill method
await page.FillAsync("#name", "John Doe");
await page.FillAsync("#email", "[email protected]");
// Select from dropdown
await page.SelectOptionAsync("#country", new[] { "us" });
// Check a checkbox
await page.CheckAsync("#newsletter");
// Click the submit button
await page.ClickAsync("#submit-button");
// Playwright automatically waits for elements to be available
var confirmationText = await page.TextContentAsync("#confirmation-message");
Assert.That(confirmationText, Is.EqualTo("Thank you for your submission!"));
// Record a video of the test (must enable when creating context)
await context.CloseAsync();
}
3. SpecFlow for BDD UI Testing
Behavior-Driven Development (BDD) combines nicely with UI testing. SpecFlow allows you to write UI tests in a human-readable format.
Installing SpecFlow
dotnet add package SpecFlow
dotnet add package SpecFlow.NUnit
dotnet add package SpecFlow.Tools.MsBuild.Generation
SpecFlow UI Test Example
First, create a feature file (LoginFeature.feature):
Feature: User Login
As a registered user
I want to log into the application
So that I can access my account
Scenario: Successful login with valid credentials
Given I am on the login page
When I enter valid username "[email protected]"
And I enter valid password "password123"
And I click the login button
Then I should be redirected to the dashboard
And I should see my username displayed
Then implement the test steps:
[Binding]
public class LoginSteps
{
private IWebDriver _driver;
private readonly ScenarioContext _scenarioContext;
public LoginSteps(ScenarioContext scenarioContext)
{
_scenarioContext = scenarioContext;
_driver = new ChromeDriver();
}
[Given(@"I am on the login page")]
public void GivenIAmOnTheLoginPage()
{
_driver.Navigate().GoToUrl("https://example.com/login");
}
[When(@"I enter valid username ""(.*)""")]
public void WhenIEnterValidUsername(string username)
{
_driver.FindElement(By.Id("username")).SendKeys(username);
}
[When(@"I enter valid password ""(.*)""")]
public void WhenIEnterValidPassword(string password)
{
_driver.FindElement(By.Id("password")).SendKeys(password);
}
[When(@"I click the login button")]
public void WhenIClickTheLoginButton()
{
_driver.FindElement(By.Id("login-button")).Click();
}
[Then(@"I should be redirected to the dashboard")]
public void ThenIShouldBeRedirectedToTheDashboard()
{
var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
wait.Until(d => d.Url.Contains("/dashboard"));
Assert.That(_driver.Url, Does.Contain("/dashboard"));
}
[Then(@"I should see my username displayed")]
public void ThenIShouldSeeMyUsernameDisplayed()
{
var usernameElement = _driver.FindElement(By.Id("user-profile"));
Assert.That(usernameElement.Text, Does.Contain("[email protected]"));
}
[AfterScenario]
public void AfterScenario()
{
_driver?.Quit();
}
}
Best Practices for UI Testing in .NET
1. Use the Page Object Model (POM)
The Page Object Model is a design pattern that creates a clean separation between test code and page-specific code:
// LoginPage.cs
public class LoginPage
{
private readonly IWebDriver _driver;
// Locators
private IWebElement UsernameField => _driver.FindElement(By.Id("username"));
private IWebElement PasswordField => _driver.FindElement(By.Id("password"));
private IWebElement LoginButton => _driver.FindElement(By.Id("login-button"));
public LoginPage(IWebDriver driver)
{
_driver = driver;
}
public void NavigateTo()
{
_driver.Navigate().GoToUrl("https://example.com/login");
}
public void EnterUsername(string username)
{
UsernameField.SendKeys(username);
}
public void EnterPassword(string password)
{
PasswordField.SendKeys(password);
}
public DashboardPage ClickLogin()
{
LoginButton.Click();
return new DashboardPage(_driver);
}
public DashboardPage LoginAs(string username, string password)
{
EnterUsername(username);
EnterPassword(password);
return ClickLogin();
}
}
// Test using POM
[Test]
public void UserCanLoginSuccessfully()
{
var loginPage = new LoginPage(_driver);
loginPage.NavigateTo();
var dashboardPage = loginPage.LoginAs("[email protected]", "password123");
Assert.IsTrue(dashboardPage.IsLoaded());
Assert.That(dashboardPage.GetWelcomeMessage(), Does.Contain("Welcome"));
}
2. Implement Explicit Waits
Instead of using Thread.Sleep()
, use explicit waits:
public void WaitForElement(By locator, int timeoutInSeconds = 10)
{
var wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(timeoutInSeconds));
wait.Until(d => d.FindElement(locator).Displayed);
}
3. Take Screenshots on Failures
Add code to capture screenshots when tests fail:
[TearDown]
public void TearDown()
{
if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
{
var screenshot = ((ITakesScreenshot)_driver).GetScreenshot();
var filename = $"Error_{TestContext.CurrentContext.Test.Name}_{DateTime.Now:yyyyMMdd_HHmmss}.png";
screenshot.SaveAsFile(filename, ScreenshotImageFormat.Png);
TestContext.AddTestAttachment(filename);
}
_driver?.Quit();
}
4. Run Tests in Parallel
To speed up test execution, consider running tests in parallel:
[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]
Testing Real-World .NET Applications
Testing ASP.NET Core Applications
When testing ASP.NET Core applications, you might want to run the application and then execute UI tests against it:
public class AspNetCoreUiTests
{
private Process _webServerProcess;
private IWebDriver _driver;
private string _baseUrl = "https://localhost:5001";
[OneTimeSetUp]
public void OneTimeSetup()
{
// Start the ASP.NET Core application
_webServerProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = "run --project ../../../src/WebApp/WebApp.csproj",
UseShellExecute = false,
CreateNoWindow = true
}
};
_webServerProcess.Start();
// Allow time for the web server to start
Thread.Sleep(5000);
}
[SetUp]
public void Setup()
{
_driver = new ChromeDriver();
}
[Test]
public void HomePage_LoadsCorrectly()
{
_driver.Navigate().GoToUrl(_baseUrl);
Assert.That(_driver.Title, Does.Contain("Home"));
// Find and interact with elements
var welcomeMessage = _driver.FindElement(By.TagName("h1"));
Assert.That(welcomeMessage.Text, Is.EqualTo("Welcome to the Demo App"));
}
[TearDown]
public void TearDown()
{
_driver?.Quit();
}
[OneTimeTearDown]
public void OneTimeTearDown()
{
// Shut down the web server
_webServerProcess?.Kill();
_webServerProcess?.Dispose();
}
}
Testing Desktop Applications with WinAppDriver
For testing Windows desktop applications built with WPF or Windows Forms, you can use WinAppDriver:
[TestFixture]
public class DesktopAppTests
{
private WindowsDriver<WindowsElement> _driver;
[SetUp]
public void Setup()
{
// Start WinAppDriver service
// (Make sure you've installed and started WinAppDriver)
var options = new AppiumOptions();
options.AddAdditionalCapability("app", @"C:\Path\To\YourApp.exe");
_driver = new WindowsDriver<WindowsElement>(
new Uri("http://127.0.0.1:4723"), options);
}
[Test]
public void ApplicationLaunchesSuccessfully()
{
// Verify the main window title
Assert.That(_driver.Title, Is.EqualTo("My Desktop App"));
// Interact with UI elements
_driver.FindElementByAccessibilityId("NameTextBox").SendKeys("John");
_driver.FindElementByName("Submit").Click();
// Verify result
var resultLabel = _driver.FindElementByAccessibilityId("ResultLabel");
Assert.That(resultLabel.Text, Is.EqualTo("Hello, John!"));
}
[TearDown]
public void TearDown()
{
_driver?.Quit();
}
}
Continuous Integration with UI Tests
Integrating UI tests into your CI/CD pipeline ensures that UI tests run automatically with each build:
Azure DevOps Pipeline Example
trigger:
- main
pool:
vmImage: 'windows-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '6.0.x'
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Install Playwright browsers'
inputs:
command: 'custom'
custom: 'tool'
arguments: 'update --global Microsoft.Playwright.CLI'
- script: playwright install
displayName: 'Install Playwright browsers'
- task: DotNetCoreCLI@2
displayName: 'Run UI tests'
inputs:
command: 'test'
projects: '**/*UITests.csproj'
arguments: '--configuration $(buildConfiguration)'
env:
HEADLESS: true
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
searchFolder: '$(Agent.TempDirectory)'
condition: succeededOrFailed()
- task: PublishBuildArtifacts@1
displayName: 'Publish screenshots'
inputs:
PathtoPublish: '$(Build.SourcesDirectory)/TestResults'
ArtifactName: 'screenshots'
condition: succeededOrFailed()
Summary
UI testing is a crucial part of ensuring your .NET applications provide the expected user experience. Through tools like Selenium WebDriver, Playwright, and SpecFlow, you can create comprehensive tests that validate your application's user interface and interactions. By following best practices such as using the Page Object Model, implementing explicit waits, and integrating your tests into CI/CD pipelines, you can build a robust UI testing strategy.
Remember that UI tests are typically slower and more brittle than unit tests, so it's important to maintain a balanced testing pyramid with more unit tests and fewer UI tests. Focus your UI tests on critical user journeys rather than trying to test every possible interaction.
Additional Resources
- Selenium Documentation
- Playwright for .NET Documentation
- SpecFlow Documentation
- WinAppDriver for Desktop UI Testing
- Microsoft Learn - Test automation for web applications
Exercises
-
Basic Exercise: Create a UI test that navigates to a public website of your choice and verifies the title and presence of key elements.
-
Intermediate Exercise: Implement the Page Object Model for a login form, and create tests for both successful and failed login attempts.
-
Advanced Exercise: Create a complete test suite for a multi-step form (like a checkout process) that includes form validation, progress tracking, and final submission.
-
Challenge: Set up a CI/CD pipeline using GitHub Actions or Azure DevOps that runs your UI tests on every push to your repository.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)