logo logo

Page Object Model in Selenium: Using C#, NUnit and Reporting

Page Object Model in Selenium: Using C#, NUnit and Reporting

Page Object Model, or simply POM, is one of the most used Design Patterns used in test automation and is often considered a best practice among test engineers. They are even listed in the official Selenium documentation guidelines. In short, what POM means is that each web page or web component of an app has a corresponding page class, that contains all element identifications and actions performed on the elements. We’ll cover all of this later, as well as how to create a Page Object Model in Selenium framework using TestProject and NUnit.

Table of Contents – Page Object Model in Selenium

  1. Prerequisites
  2. Benefits of using Page Object Model in Selenium
  3. Why Use TestProject SDK in Your Project
  4. Using CSS Selector as Selenium Locators
  5. Our NUnit Testing Framework
  6. The First Page Object Class
  7. The TestProject Reports
  8. The Second Test
  9. Summary

Prerequisites

Before you start, make sure you have everything you need in place:

The Benefits of Using Page Object Model in Selenium

If you are not familiar with POM, you might ask yourself what exactly makes it so great?

  • It helps avoid code duplication by following the DRY (Don’t repeat yourself) principle. Think about the login feature of an app. You might need to use it each time you run a test, but it’s much better to have one login method in a page class and call it in every test where it’s needed.
  • It makes the tests more readable. See below an example of a test that doesn’t use POM:
driver.FindElementByCssSelector("#name").SendKeys("John Smith");
driver.FindElementByCssSelector("#password").SendKeys("12345");
driver.FindElementByCssSelector("#login").Click();

if (driver.FindElement(By.CssSelector("#logout")).Displayed)
{
return ExecutionResult.Passed;
}
return ExecutionResult.Failed;

And compare it with this one:

homePage.Login();
if (homePage.IsLoginSuccessful())
{
return ExecutionResult.Passed;
}
return ExecutionResult.Failed;

The second code sample is much easier to read – you can clearly see that a login action is performed, and then checked that it was successful. The rest of the code is moved inside the homepage class.

  • It reduces the time it takes to refactor the tests. When a change was made on a web page, the code also needs to be updated to reflect this change. Using POM, the code only needs to be updated in the page class.

Why Use TestProject SDK in Your Project

Using TestProject offers many benefits to the project.

  • Easy to use. If you are already familiar with Selenium, there is not a lot more to learn, you just need to set up your account, install the TestProject SDK and start writing tests using standard Selenium commands.
  • Reporting capabilities. With very little setup, again just adding the SDK to the project and running the agent, TestProject automatically creates HTML reports in your account. These reports are also downloadable in PDF format, so you can email them or save them locally.
  • Allows logging. The TestReporter class includes a method that allows you to take screenshots, and also to log messages, depending on the step result. If the steps throw an exception, it will be displayed in the report, but the best practice would be to include a log message as well, especially if you want to catch certain exceptions.
  • Browser management. No need to download and update the drivers each time a new version is available. In the RunnerBuilder, you can simply select the type of tests you need to run (Web, Android, iOS), and then select the browser you need. TestProject currently supports Chrome, Firefox, Internet Explorer, Safari, Opera, and Edge.
  • Can be used as part of a CI/CD flow. TestProject’s API allows integration with any CI/CD tool of your choice, or use built-in integrations to Azure DevOps, Jenkins, TeamCity or GitHub.

Using CSS Selector as Selenium Locators

Web elements can be identified by different locators in Selenium: ID, name, class name, link (or partial link) text, tag, DOM locator, CSS selector, and XPath.

The easiest method of locating an element is by ID, which is usually unique. However, not all elements have IDs, or these IDs can be dynamically generated, making it harder to automate the element interaction, since the ID is always changing.

This is where CSS selectors are useful: they allow using a combination of the element’s attributes, including its values, or its parents or siblings. Compared with XPath, CSS selectors work faster and have greater browser compatibility. They are also easier to read and understand: “#greetings > b” versus “//p[@id=’greetings’]/b”.

To obtain the CSS selector from the browser, use the browser’s developer tools, which will show the HTML source of the page. You can also right-click on an element on the page and select Inspect. Then, right-click on the element in the Developers tool and select Copy selector.

CSS Selector from Developer Tools

Here are some useful CSS selectors examples:

  • .form-control – selects the element with the class name “form-control”
  • #name – selects the element with the ID “name”
  • input #name – selects the element with the tag “input” and the id “name”
  • input ~div – selects the div element that is preceded by an input element
  • form > div – selects the div element where the parent is a form element
  • div:nth-child(2) – selects the div element that is the second child of its parent
  • input[id=’name’] – selects the input element id “name”

CSS selectors also allow combinations of all these attributes.

Going forward, we will use CSS selectors as our Selenium locators.

Our NUnit Testing Framework

TestProject can be used with various testing frameworks, but for our demo project, we will use the NUnit testing framework. At its core, NUnit is a unit testing framework for .Net languages. It’s a great framework, even if you don’t have experience with it, because, since it is widely used, there are a lot of learning resources online, apart from their documentation which is available on their website.

When creating a new project, Visual Studio allows you to select the NUnit project type, so let’s go ahead and do that:

New Visual Studio NUnit project

Then add the TestProject C# SDK to the project: right-click on the project name, Manage NuGet Packages, and browse for TestProject.SDK:

TestProject SDK NuGet package

That’s all, now we can move on to writing the page classes and tests.

The First Page Object Class

Let’s use the https://example.testproject.io/web/ page for our tests. It’s a simple web application, that has a login form, and after the user logs in, it loads a nice form that can be saved, and it allows the user to log out.

The page also has some other elements, but to keep our page class efficient, we will only implement the elements that we need in our tests.

So, for our first test, we will do a simple login, with the username John Smith and the password 12345. To check that the login was successful, we’ll check that the correct name is displayed on the page:

Logged in user

To do this, we will create a new class for our home page. It’s good practice to keep the page objects separate from the tests, so start by creating a new folder inside the project, e.g. “Pages”, and add a new class here “HomePage.cs”.

What we need first in this class is the driver and a constructor that takes the driver as a parameter, because we are going to pass the driver as a parameter from our test class, later. The class should look like this:

public class HomePage
    {
        private IWebDriver driver;
        public HomePage(IWebDriver driver)
        {
            this.driver = driver;
        } 
    }

Now, let’s see what actions we need to perform to log in: insert a username, insert a password, and click the Login button.

Login form

In Chrome, right-click on the name field and select Inspect, and you’ll get this in the Elements tab of Developer tools:

HTMLS code

You can now use the ID to identify the element. You can also right-click on the line in Developer tools and select Copy > Copy selector, and you will have the CSS selector in your clipboard. Then do the same for the other fields:

private IWebElement Name => driver.FindElement(By.CssSelector("#name"));
private IWebElement Password => driver.FindElement(By.CssSelector("#password"));
private IWebElement LoginButton => driver.FindElement(By.CssSelector("#login"));

Now we need to interact with our elements. Since it’s probably rare to just interact with one of them, a single method for login where we send all the data should be enough:

public void Login()
        {
            Name.SendKeys("John Smith");
            Password.SendKeys("12345");
            LoginButton.Click();
        }

Next, we need to verify that the login was successful, and the name is displayed on the new page. So, let’s inspect the element and see how we can identify it:

HTML code

The locator for the element will be the b element that has a parent with the id “greetings”, so it should look like this:

private IWebElement DisplayName => driver.FindElement(By.CssSelector("#greetings > b"));

Next, we need a method that verifies that the name is displayed. However, it’s bad practice to include assertions in the page class, those should be used only in the test methods, so we will create a method that returns a Boolean:

public bool IsLoginSuccessful()
        {
            return DisplayName.Text.Equals("John Smith");
        }

This is how the class should look like now:

using OpenQA.Selenium;

  namespace TestProject_POM.Pages
  {
      public class HomePage
      {
          private IWebDriver driver;
          public HomePage(IWebDriver driver)
          {
              this.driver = driver;
          }
          private IWebElement Name => driver.FindElement(By.CssSelector("#name"));
          private IWebElement Password => driver.FindElement(By.CssSelector("#password"));
          private IWebElement LoginButton => driver.FindElement(By.CssSelector("#login"));
          private IWebElement DisplayName => driver.FindElement(By.CssSelector("#greetings > b"));
          public void Login()
          {
              Name.SendKeys("John Smith");
              Password.SendKeys("12345");
              LoginButton.Click();
          }	

          public bool IsLoginSuccessful()
          {
              return DisplayName.Text.Equals("John Smith");
          }
      }
  }

You can see in this class that the only library we are using is the one provided by Selenium.

Adding the test

Now, let’s actually use TestProject in our project. We’ll create a new class, “LogInTests.cs” that will look like this:

using OpenQA.Selenium;
using TestProject.Common.Enums;
using TestProject.SDK;
using TestProject.SDK.Interfaces;
using TestProject.SDK.Reporters;
using TestProject.SDK.Tests;
using TestProject.SDK.Tests.Helpers;
using TestProject_POM.Pages;	

namespace TestProject_POM
  {
      public class LogInTest : IWebTest
      {
          ExecutionResult IWebTest.Execute(WebTestHelper helper)
          {
              var driver = helper.Driver;
              TestReporter report = helper.Reporter;
              HomePage homePage = new HomePage(driver);	

              driver.Navigate().GoToUrl("https://example.testproject.io/web/");	

              homePage.Login();
              helper.Reporter.Step("Logged in the app", "The login is unsuccessful", "The login is successful",
                  homePage.IsLoginSuccessful(), TakeScreenshotConditionType.Always);	

              if (homePage.IsLoginSuccessful())
              {
                  return ExecutionResult.Passed;
              }
              return ExecutionResult.Failed;
          }
      }
  }

Let’s take a step back and analyze the code:

  • The class extends the IWebTest interface. This is one of the 3 interfaces provided by TestProject (IWebTest, IAndroidTest, and IIOSTest) that implements the actual test execution.
  • The Execute method returns an ExecutionResult enum, the available values being Passed, Failed, and Suspended
  • This block of code:
TestReporter report = helper.Reporter;
HomePage homePage = new HomePage(driver);
driver.Navigate().GoToUrl("https://example.testproject.io/web/");
homePage.Login();

Creates new instances of the TestReporter class and the HomePage. The first one will help us in the creation of the final TestProject report, and the second one allows us to use the methods that we defined in our page class. The third line just opens the web page in the browser, and it uses Selenium methods, and the last one calls the Login method from the page class which will perform the login for us.

  •  The Reporter.Step method allows us to log more detailed information in our report – such as different messages for success and failure, and also takes a screenshot. For this test, let’s make the test always take a screenshot, but keep in mind that you can choose to do this only on success, or only on failure, or don’t take a screenshot at all.

TestReporter Step method

  • The last part is verifying that the login was successful, so here we check the value of the IsLoginSuccessful method in our test class, and if it’s true, the test will pass, if not, it will fail.

Now, to get the tests to run, create a new class using the NUnit Framework. Here, set up the TestProject runner, choose the browser you want to run the test on, and create test the method. Here’s how the class should look like:

using NUnit.Framework;
using TestProject.Common.Enums;
using TestProject.SDK;
using System;

namespace TestProject_POM
  {
      [TestFixture]
      public class LogInTests
      {
          Runner runner;
          private string DevToken = Environment.GetEnvironmentVariable("TP_DEV_TOKEN");	

          [OneTimeSetUp]
          public void SetUp()
          {
              runner = new RunnerBuilder(DevToken).AsWeb(AutomatedBrowserType.Chrome).Build();
          }	

          [Test]
          public void Login()
          {
              runner.Run(new LogInTest());
          }
          
          [OneTimeTearDown]
          public void TearDown()
          {
              runner.Dispose();
          }
      }
  }

The TP_DEV_TOKEN is the environment variable that contains the TestProject token, if you named it something else, replace it in the code as well.

In the SetUp() method, we create the runner, where we can define the test type (AsWeb), the browser where we’ll run the tests (Chrome), and we can also add some custom parameters, such as WithJobname and WithProjectName, which allow us to define custom names for our project and job.

The [OneTimeSetUp] is an NUnit attribute, for methods that run once before any of the tests. If the run contains multiple tests, this method will only execute once. The same goes for the [OneTimeTearDown] attribute, which identifies the method that will run after all the child tests have run. In our case, this method will dispose of all resources used by the runner.

The remaining method is the [Test] method – the NUnit attribute is self-explanatory. Here, we just give as runner parameter the test we want to run.

Page Object Model in Selenium – The TestProject HTML Reports

After building the page object model in Selenium solution, run the test and check the Reports tab in the TestProject web app and click on the Project Name. The HTML reports for the test execution will be automatically loaded and should look like this:

Page Object Model in Selenium: TestProject HTML report

See how it displays the custom job name (‘Page Object Tests”), the custom project name (“POM Project”), and other useful information:

  • The test results
  • The time and date
  • The time it took to run the job
  • The agent used
  • The number of tests
  • The test steps

Apart from the statistics, on the right side of the test report, you can expand each test step and see its details. For example, here is the outcome of the IsLoginSuccessful() method:

Test report step details

You also have the option to download in PDF format, either a Summary report, that displays the statistics of the tests and information of the failures, or a Full report, which has all the information contained in the HTML report, including screenshots and custom messages:

PDF report step with screenshot

The Second Test

Maybe you’re not convinced yet. Why do we need so many classes when all we do is perform a simple login? You may be right, but in real life, it is very unlikely to have a test framework that only has one test for an entire web page. Even for the login, it’s useful to have more than one test – you may need, for example, to test the login for multiple types of users, or test the form with invalid credentials. Let’s do that in our framework.

First, we need to change the Login class, so instead of sending the credentials as hard-coded strings, we send them as parameters:

public void Login(string name, string password)
        {
            Name.SendKeys(name);
            Password.SendKeys(password);
            LoginButton.Click();
        }

Now we need to update the test to send this data, otherwise the code won’t compile:

string username = "John Smith";
string password = "12345";
//the rest of the code remains unchanged
homePage.Login(username, password);

For an invalid login, the application returns a “Password is invalid message”, so this is what we need to verify in our tests:

failed login - Invalid password

So for the second test, create a new “InvalidLoginTest” class:

using OpenQA.Selenium;
using TestProject.Common.Enums;
using TestProject.SDK;
using TestProject.SDK.Interfaces;
using TestProject.SDK.Reporters;
using TestProject.SDK.Tests;
using TestProject.SDK.Tests.Helpers;
using TestProject_POM.Pages;

namespace TestProject_POM
{
    public class InvalidLoginTest : IWebTest
    {
        ExecutionResult IWebTest.Execute(WebTestHelper helper)
        {
            var driver = helper.Driver;
            TestReporter report = helper.Reporter;
            HomePage homePage = new HomePage(driver);

            driver.Navigate().GoToUrl("https://example.testproject.io/web/");

            homePage.Login("Invalid Login", "1234");
            helper.Reporter.Step("Incorrect password messsage", "The message is not displayed", "The message is displayed",
                homePage.IsPasswordIncorrectMessageDisplayed(), TakeScreenshotConditionType.Always);

            if (homePage.IsPasswordIncorrectMessageDisplayed())
            {
                return ExecutionResult.Passed;
            }
            return ExecutionResult.Failed;
        }
    }
}

This time everything should be pretty straight-forward, we are using the same Login method, but sending different data to it, and the assertion part of the test verifies that the incorrect message is displayed in the page, using the IsPasswordIncorrectMessageDisplayed() method. Now we need to implement this method in the HomePage class – it will again be a method that return the bool type:

public bool IsPasswordIncorrectMessageDisplayed()
        {
            return IncorrectPasswordMessage.Text.Equals("Password is invalid");
        }

Let’s inspect the message element in Developer tools, to find the best identifier for the message:

inspect the message element in Developer tools

You’ll notice that if you try to search all the elements with the class name “invalid-feedback“, you’ll find 6 different elements, so the class name cannot be used as a unique identifier for the message. Again, let’s try to create a CSS selector based on unique attributes of the elements: the password input element in the page is the only element with the id “password”, and the message we’re looking for is the only div that follows it, so we can use this:

private IWebElement IncorrectPasswordMessage => driver.FindElement(By.CssSelector("#password ~div"));

To actually run this tests, we need to use our NUnit test fixture class, and add the new test:

[Test]
public void TestInvalidLogin()
{
     runner.Run(new InvalidLoginTest());
}

This is all, after rebuilding the project, the Test Explorer will display both tests, and they can run successfully. The reports will now automatically display the results for both tests:

HTML report for 2 tests

You can also clone the final version of the project from GitHub and run it using your agent and developer token.

Summary

So that’s all it takes to create your first project using Page Object Model in Selenium, NUnit, and reporting provided by TestProject. You can of course expand the tests to include even more scenarios, and new page classes added to keep the functionalities separate.

Happy Testing! 🤩

About the author

Andreea Draniceanu

Andreea is a QA engineer, experienced in web and desktop applications, and always looking to improve her automation skills and knowledge. Her current focus is automation testing with C#.

Comments

17 1 comment

Leave a Reply

FacebookLinkedInTwitterEmail