How to test web applications with Playwright and C# .NET

September 19, 2022
Written by
Néstor Campos
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Testing your applications is a fundamental part of any project, whether it is to find errors during development, or to verify the expected behavior of your application in each action that a user may or may not execute.

In this post, you are going to use a simple form and test it using Playwright and C# to verify its operation with both correct and incorrect information, simulating the actions of users.

Prerequisites

You will need the following for your development environment:

Test types

You can create different types of tests to verify your software:

  • Unit tests test individual methods and functions of the classes, components, or modules your software uses.
  • Integration tests verify that the different modules or services used by your application work well together.
  • End-to-End (E2E) tests verify that your application works as expected from a user perspective by simulating how a user interacts with your application.

For more details on these types of tests and why you should do them, check out "Unit, Integration, and End-to-End Testing: What’s the Difference?".

There are a lot more types of tests, but these the three above are the most common to test your software. You'll be developing E2E tests using Playwright in this tutorial.

What is Playwright?

Playwright is a browser automation framework for testing web applications end-to-end. Playwright takes advantage of the capabilities of current browsers through an intuitive API. It addresses a lot of the pain points that you had to deal with when using older browser automation frameworks.

One of the biggest pain points of using older automation frameworks, is that you had to manually install browsers and their compatible web drivers, but over time the version of the browser would change and become incompatible with the web driver. Thus, you had to find which newer version of the web driver you had to install. And you had to do this for every browser you wanted to run your tests in. Playwright, on the other hand, automatically install the latest browsers for you and Playwright doesn't use web drivers, so you don't have to worry about keeping those in sync.

Because older automation frameworks used web drivers to instruct the web browsers, they were limited by the capabilities offered by the web drivers. Playwright use the Chrome DevTools protocol to communicate with Chromium, Chrome, and Edge browser. For other browsers like WebKit, Safari, and Firefox, Playwright extended the browser to support a similar protocol. This allows Playwright to use the DevTools protocol to efficiently communicate with all these browsers and take advantage of all their features.

In addition to supporting multiple browsers, Playwright has cross-platform and cross-language support with APIs for .NET (C#), TypeScript, JavaScript, Python, and Java.

If you don't want to manually write your tests, you can generate tests using Codegen which will open a browser and record your actions in that browsers. Each recorded action is translated into the Playwright APIs in the programming language of your choice.

You can also record videos of the tests that will allow you to play them back, which can help with figuring out why a test failed. You can also emulate devices and instantiate browsers with properties such as viewport, location, time zone, geolocation, among others.

Set up the sample web app

To get started, you must first clone this GitHub repository, which contains an ASP.NET Core web app with only 3 pages. A home page with a button leading to the form, the form page with 4 fields and some validations that you are going to test, and a success page if the form is completed successfully. While this sample web app is built using ASP.NET Core, it could've been built using any web technology and you'd still be able to write the same E2E tests with Playwright.

Open a terminal and clone the repository with the following command:

git clone https://github.com/nescampos/tdv_testwebapps.git

Then go into the tdv_testwebapps/ContactForm.Web project folder of the cloned repository and run the ASP.NET Core application:

cd tdv_testwebapps/ContactForm.Web
dotnet run

To run the project in VS Code, press Ctrl + F5 and a prompt will be displayed to select the environment, where you must select .NET Core.

The output of the ASP.NET Core application will print one or more localhost URLs where it is hosted. Open a browser and navigate to one of the localhost URLs..  You will see the home page with the button to open the form. Click on the button, fill out the form, and verify that you get to the success screen.

Home page of the web application to be tested with a button that opens a form on another page.
Home page of the sample web app
Page of the web app that contains the contact form, with the fields
Contact us page of the sample web app
Thank you page after completing the form successfully.
Contact us success page of the sample web app

You will be testing this application, so leave it running in the background.

Now that you're familiar with the sample web app, let's find out how to test this web app programmatically.

Create the NUnit project

There are many frameworks to test .NET applications, with xUnit, NUnit, and MSTest being the most popular. This tutorial will use NUnit as its testing framework.

Open another terminal and navigate where you cloned the git repository, then create an NUnit project called ContactForm.Tests, and navigate to the new folder:

dotnet new nunit -n ContactForm.Tests
cd ContactForm.Tests

Install the Playwright library

Now, to use Playwright, you must install it as a library in the project. Run this command to add the Playwright NUnit NuGet package:

dotnet add package Microsoft.Playwright.NUnit

The Playwright library requires some extra setup, which is done via a PowerShell script. This is a little odd for a .NET package, but to get the script you first need to build the project, so go ahead and build the project:

dotnet build

Now the Playwright dependency has been copied into your bin folder, and you can run the PowerShell script. Run the installation script like this:

pwsh bin/Debug/net6.0/playwright.ps1 install

Replace net6.0 with your version of .NET if you're not using .NET 6.

This installation script will install Playwright into your project, including the browsers.

Test the web app

Test the home page

Now, with Playwright installed in the NUnit project, you are going to create your first test, in which you will check if there is a button to open the form on the home page and verify that clicking the button navigates the browser to the form page.

Open the test project in your IDE and delete the generated UnitTest1.cs file. To test the home page, create a new file called WebAppTest.cs in the project and copy the following code into the file:

using Microsoft.Playwright.NUnit;
using NUnit.Framework;
using System.Threading.Tasks;
using System.Text.RegularExpressions;

[Parallelizable(ParallelScope.Self)]
public class Tests : PageTest
{
    [Test]
    public async Task Clicking_ContactButton_Goes_To_ContactForm()
    {
        await Page.GotoAsync("<URL>");
        var formButton = Page.Locator("text=Open Contact Form");
        await formButton.ClickAsync();
        await Expect(Page).ToHaveURLAsync(new Regex(".*Home/Form"));
    }
}

Replace <URL> with the web app URL.

In the above code, you have just created a test that retrieves the button to open the form, clicks on it, and checks if the next page is the form page based on the current URL.

By default, Playwright will run your code against a browser in headless mode, meaning that the browser will not be visible. To make it easier to debug and see what's actually happening, you can turn off Headless mode. However, the tests would run so fast that you still can't perceive what is happening, which is why Playwright provides a SlowMo option that you can use to slow down the steps within your test.

The most flexible way to configure these options, is to use a .runsettings file. .runsettings files are used by all .NET testing frameworks to configure how the tests run, and Playwright added support to configure Playwright using .runsettings files as well.

Create a new file called .runsettings with the following content:

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <Playwright>
    <BrowserName>chromium</BrowserName>
    <LaunchOptions>
      <Headless>false</Headless>
      <Channel>msedge</Channel>
      <SlowMo>500</SlowMo>
    </LaunchOptions>
  </Playwright>
</RunSettings>

Here's what the configuration means:

  • BrowserName lets you pick between the chromium, firefox, or webkit browser.
  • Headless is set to true by default, but you can set it to true so that you can see the browser UI as the test is run.
  • Channel configures which version of the browser to run. When you pick chromium as the browser name, you can pick chrome or msedge to use Google Chrome or Microsoft Edge which are both browser built on top of Chromium. You can also choose to test against the beta version and dev version by suffixing the channel with -beta or -dev, for example msedge-beta. You can learn more about the supported browsers and channels at the Playwright docs.
  • SlowMo lets you slow down automation steps by the specified amount of miliseconds.

Now, run the test using the following command:

dotnet test --settings .runsettings

If everything is ok, you will see the browser will open, click on the specified button and then close.

In the console you will see the result of the successful execution of the test, with an output like the following:

Correct! - With error:     0, Pass:     1, Skip:     0, Total:     1, Time: 5 s - ContactFormTests.dll (net6.0)

You can also run tests from VS Code with .runsettings, and Visual Studio, and there are also settings for this in JetBrains Rider.  

Test the form

Now, you are going to create a test that fails. In this test, you will enter all the data of the form at another URL correctly, except for the first name, which in the form will be empty, but hoping that the data is valid and you arrive at the page of thanks.

Since the form is in the same web application, but in another URL, you are going to optimize this using a variable where you will store the base of the URL. And instead of hardcoding this variable, you can make the project more flexible by configuring the URL using the .runsettings file.

First, add the WebAppUrl parameter in the .runsettings file:

<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <TestRunParameters>
    <Parameter name="WebAppUrl" value="<URL>" />
  </TestRunParameters>
  <Playwright>
    <BrowserName>chromium</BrowserName>
    <LaunchOptions>
      <Headless>false</Headless>
      <Channel>msedge</Channel>
      <SlowMo>200</SlowMo>
    </LaunchOptions>
  </Playwright>
</RunSettings>

Replace <URL> with the web app URL.

Turning off Headless and using SlowMo helps for debugging, but turning Headless on and SlowMo off will increase the performance of your tests. For local development, it's up to your preference, but in production and CI environments, you should use keep Headless on and SlowMo off.

Next, add a method called Init(), which, unlike the test methods, will have the OneTimeSetUp attribute to specify that it should be executed once and before the tests. In this method, you are going to get the value of the URL parameter in the configuration file and save it in the variable webAppUrl. Then use the webAppUrl variable in the existing test instead of the hard-coded URL. Your code should look like this:

using Microsoft.Playwright.NUnit;
using NUnit.Framework;
using System.Threading.Tasks;
using System.Text.RegularExpressions;

[Parallelizable(ParallelScope.Self)]
public class Tests : PageTest
{
    public static string webAppUrl;

    [OneTimeSetUp]
    public void Init()
    {
        webAppUrl = TestContext.Parameters["WebAppUrl"] 
                ?? throw new Exception("WebAppUrl is not configured as a parameter.");
    }

    [Test]
    public async Task Clicking_ContactButton_Goes_To_ContactForm()
    {
        await Page.GotoAsync(webAppUrl);
        var formButton = Page.Locator("text=Open Contact Form");
        await formButton.ClickAsync();
        await Expect(Page).ToHaveURLAsync(new Regex(".*Home/Form"));
    }
}

Now, create the new test with the following code:

    [Test]
    public async Task Filling_And_Submitting_ContactForm_Goes_To_SuccessPage()
    {
        await Page.GotoAsync($"{webAppUrl}/Home/Form");
        await Page.Locator("text=First name").FillAsync("");
        await Page.Locator("text=Last name").FillAsync("Campos");
        await Page.Locator("text=Email address").FillAsync("nestor@gmail.com");
        await Page.Locator("text=Birth date").FillAsync("1989-03-16");
        await Page.Locator("text=Send").ClickAsync();
        await Expect(Page).ToHaveURLAsync(new Regex(".*Home/Success"));
    }

This test navigates directly to the form page, then fills out each form input, submits the form, and verifies the submission was succesful by checking current page is the success page.

The text= locator in the above test will retrieve the labels of the form input, not the form input itself, but because the labels have been correctly linked to the form input using the for and id attribute, Playwright fills in the text into the correct inputs.

Playwright supports a lot of different selectors to locate elements, such as CSS selectors, XPath queries, by ID or test ID, etc. In your tests, you've been using the text-selector which finds elements by matching visible text. Playwright recommends using user-facing attributes to select your elements such as the text, since these don't change as often as the underlying DOM structure.

Rerun the tests, but this time, you're just specifying the execution of the new test by filtering the tests by name:

dotnet test --settings .runsettings --filter Filling_And_Submitting_ContactForm_Goes_To_SuccessPage

You will see the browser open, fill in the data and try to send it, but it fails because the name is required and the test expects to hit a success page.

With error! - With error:     1, Pass:     0, Skip:     0, Total:     1, Time: 8 s - ContactFormTests.dll (net6.0)

Fix the value in the first name with a valid name to pass the test:

await Page.Locator("text=First name").FillAsync("Néstor");

And run the same command again which should show this output:

Correct! - With error:     0, Pass:     1, Skip:     0, Total:     1, Time: 5 s - ContactFormTests.dll (net6.0)

Test form validation

Instead of verifying the "happy path" where the form submits successfully, let's test one of the validation errors by not filling out a valid email address.

Add the following test after your existing tests:

    [Test]
    public async Task Filling_Invalid_Email_Should_Show_ValidationError()
    {
        await Page.GotoAsync($"{webAppUrl}/Home/Form");

        ILocator emailValidationLocator = Page.Locator("text=The Email address field is not a valid e-mail address.");
        await Expect(emailValidationLocator).Not.ToBeVisibleAsync();

        await Page.Locator("text=Email address").FillAsync("nestorgmail.com");
        await Page.Locator("text=Send").ClickAsync();

        await Expect(Page).ToHaveURLAsync(new Regex(".*Home/Form"));
        await Expect(emailValidationLocator).ToBeVisibleAsync();
    }

This test will verify that there's no invalid email address warning present before submitting the form. Then it will enter an invalid email address, submit the form, and verify that the current page is still the form page via URL and also verify that the email validation error message is visible.

Run the following command to run this test:

dotnet test --settings .runsettings --filter Filling_Invalid_Email_Should_Show_ValidationError

You can apply the same techniques to test all variations of validation.

Next Steps

You just wrote some end-to-end tests to verify form functionality using Playwright. You've learned about a couple of Playwright features and how to use the library, but only scratched the surface. I recommend exploring the Playwright documentation to learn more, for example, you can obtain screenshots of each test, to be able to verify or document your tests with images, or even record videos.

Additional resources

Check out the following resources for more information on the topics and tools presented in this tutorial:

Playwright for .NET – This page contains all the information about the requirements and examples with Playwright for .NET.

Playwright GitHub - Code repository on GitHub from Microsoft containing the Playwright project.

Source Code to this tutorial on GitHub - You can find the source code for this project at this GitHub repository. Use it to compare solutions if you run into any issues.

Néstor Campos is a software engineer, tech founder, and Microsoft Most Value Professional (MVP), working on different types of projects, especially with Web applications.