When building web applications with .NET Core, ensuring your app behaves as expected across browsers and user scenarios is critical. Unit and integration tests cover business logic and API layers well, but they can’t replicate real user interactions in a browser — that’s where UI testing (or end-to-end testing) shines.
Playwright is a modern, powerful, cross-browser automation framework by Microsoft, designed for reliable and fast UI tests. It supports Chromium, Firefox, and WebKit, allowing you to test your app as a user would, across different environments.
Why Use Playwright for .NET UI Testing?
- Multi-browser support: Test on Chrome, Firefox, Safari with a unified API.
- Auto-waiting: Playwright intelligently waits for elements to be ready before interacting, reducing flaky tests.
- Network interception: Mock API responses to isolate frontend behavior.
- Cross-platform: Run tests locally or in CI environments, headless or headed.
- Native .NET SDK: Write tests directly in C# with full .NET integration.
Setting Up Playwright in Your .NET Core Project
1. Add Playwright NuGet Package
In your test project (e.g., an xUnit test project), run:
dotnet add package Microsoft.Playwright
This adds the Playwright SDK for .NET.
2. Install Browsers
Playwright requires browser binaries. Run this CLI command once to download the necessary browsers:
playwright install
Alternatively, you can run it programmatically from your tests, but running it once manually is recommended.
Writing Your First Playwright Test
Create a test class in your test project, for example:
using Microsoft.Playwright;
using Xunit;
public class UiTests : IAsyncLifetime
{
private IPlaywright _playwright;
private IBrowser _browser;
// Setup before tests
public async Task InitializeAsync()
{
_playwright = await Playwright.CreateAsync();
_browser = await _playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions { Headless = true });
}
// Cleanup after tests
public async Task DisposeAsync()
{
await _browser.CloseAsync();
_playwright.Dispose();
}
[Fact]
public async Task HomePage_ShouldDisplayWelcomeMessage()
{
// Create a new browser context (like a fresh user session)
var context = await _browser.NewContextAsync();
// Open a new page/tab
var page = await context.NewPageAsync();
// Navigate to your local app (adjust URL as needed)
await page.GotoAsync("https://localhost:5001");
// Select the h1 heading and get its text content
var headingText = await page.TextContentAsync("h1");
// Assert the heading is as expected
Assert.Equal("Welcome to MyApp!", headingText);
}
}
Explanation:
- IAsyncLifetime interface lets you setup and teardown Playwright resources asynchronously before/after tests.
Playwright.CreateAsync()
initializes Playwright.Chromium.LaunchAsync()
opens a new headless Chrome browser instance.NewContextAsync()
creates an isolated browser context (like a new incognito session), helping avoid test pollution.GotoAsync()
loads your app.TextContentAsync()
reads visible text for assertions.Assert.Equal()
checks test expectations.
Going Deeper: Handling User Interaction and Waiting
UI tests often involve clicking buttons, filling forms, waiting for async events, and navigating pages.
Example: Filling a form and submitting:
await page.FillAsync("#email", "test@example.com");
await page.FillAsync("#password", "supersecret");
await page.ClickAsync("button[type=submit]");
// Wait for navigation after submit
await page.WaitForURLAsync("https://localhost:5001/dashboard");
// Confirm user greeted on dashboard
var welcome = await page.TextContentAsync(".welcome-message");
Assert.Contains("Welcome back", welcome);
Notes on waiting:
- Playwright automatically waits for elements before actions (click, fill).
- For navigation, explicitly wait with
WaitForURLAsync
. - You can also wait for specific elements to appear:
await page.WaitForSelectorAsync(".success-message");
Advanced Scenarios: Mocking API Responses
You can intercept network calls to simulate backend behavior, helpful for isolating frontend tests:
await page.RouteAsync("**/api/products", async route =>
{
var json = "[{\"id\":1, \"name\":\"Test Product\"}]";
await route.FulfillAsync(new RouteFulfillOptions
{
ContentType = "application/json",
Body = json,
Status = 200
});
});
await page.GotoAsync("https://localhost:5001/products");
var productName = await page.TextContentAsync(".product-name");
Assert.Equal("Test Product", productName);
Running Playwright Tests in CI/CD
Make sure your CI environment:
- Installs .NET SDK and Playwright package.
- Runs
playwright install
to fetch browsers. - Runs tests in headless mode.
Example GitHub Actions snippet:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: '7.0.x'
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Tests
run: dotnet test --logger "trx"
Best Practices for Stable UI Tests
- Use stable selectors: Prefer data-test-id attributes or unique IDs, avoid fragile CSS selectors.
- Isolate tests: Use new browser contexts to avoid shared state.
- Seed test data: Control your backend state for predictable tests.
- Parallelize tests: Playwright supports parallel runs to speed up CI.
- Capture failures: Enable screenshots, videos, and trace logs on failure to debug flaky tests.
Conclusion
Playwright offers .NET developers a robust, modern toolkit for automating end-to-end UI tests. Its rich features, cross-browser support, and native .NET integration empower teams to build reliable tests that mimic real user behavior.
By investing in UI testing early, you reduce regressions, improve app quality, and gain confidence in releases — critical for modern .NET Core web apps.