A simpler way of writing POM classes for Test Automation in dotnet and C#

Test Automation is a never ending journey. Here's maybe a better way to write your POM classes to make them more readable and reusable throughout multiple projects.

A simpler way of writing POM classes for Test Automation in dotnet and C#
Photo by Max Duzij / Unsplash

Here's how I've recently been writing my Page Object Model (POM) classes for test automation. In reality, there's no single best way to do it.

Current tech stack

One of the most popular frameworks for using the POM design pattern with dotnet is PageFactory. Recently, it has been updated to accommodate Selenium 4. It previously only supported version 3.x.

For a new test automation project, we've switched to using the latest version.

Limitation

One notable limitation of this library is timeout errors - when Selenium WebDriver tries to locate a dynamic element. This can be a common occurrence with highly dynamic Web App such as Single Page Applications (SPA).

Here's an example:

public class TestPage
{
    private IWebDriver Driver;
    private readonly string _pageUrl;

    public TestPage(IWebDriver driver, IConfiguration configuration)
    {
        Driver = driver;
        PageFactory.InitElements(Driver, this);
        _pageUrl = configuration.GetSection("TestPageUrl").Value;
    }

    // Here attributes are used whose value will be determined dynamically
    // there's no way to reuse the selector easily
    [FindsBy(How = How.CssSelector,
        Using =
            "app-Test .intro .description .actions .button.secondary")]
    private IWebElement NeedHelpButton { get; set; }

    [FindsBy(How = How.CssSelector,
        Using =
            "app-Test app-select .selected-values")]
    private IWebElement TagsFilter { get; set; }

    [FindsBy(How = How.CssSelector,
        Using =
            "#demo-releases app-select .select-dropdown form .header .clear button")]
    private IWebElement TagsFilterClearButton { get; set; }

    [FindsBy(How = How.CssSelector,
        Using =
            "#demo-releases app-select .select-dropdown form .footer button")]
    private IWebElement TagsFilterSaveButton { get; set; }

    [FindsBy(How = How.CssSelector,
        Using =
            "#demo-releases app-select .select-dropdown form .list app-checkbox .checkbox input")]
    private IWebElement TagsFilterFirstValue { get; set; }
    
    
    [FindsBy(How = How.CssSelector,
        Using =
            "#demo-releases app-select .select-dropdown .active")]
    private IWebElement TagsFilterDropDown{ get; set; }


    public string GetPageUrl()
    {
        return _pageUrl;
    }

    public void GoToPage()
    {
        Driver.Navigate().GoToUrl(_pageUrl);
    }

}

You can have an implicit wait but sometimes you want to perform an action such as waiting if a button appears, becomes clickable and then click on the button.

You can't do this type of wait out of the box without using the Wait Helpers libraries and therefore the explicit wait. These helper methods take as parameter a By type. This is not possible with PageFactory itself as you define the selectors once as attributes without the possibility of reusing them. In theory, you can do it, but it's a lot of work when easier ways exist.

Nevertheless, the PageFactory library remains an excellent choice that's straightforward for use that covers most scenarios, making elements highly reusable.

There are pros to using this library:

  • easy to use
  • intuitive
  • easy to read at a glance
  • built-in reuse of elements

From the examples I check on the Internet, a better way is to define the properties as By. You can do so in the definition as well or in the constructor or in the property get method.

    public class Aside
    {
        private IWebDriver _driver;
        // the By elements are public so that we can use Explicit waiters
        public By AsideElement;
        public By AsideHeader;
        public By CloseButton;

        public Aside(IWebDriver driver)
        {
            _driver = driver;
            // you can also init them directly above
            AsideElement = By.CssSelector("aside a-template");
            AsideHeader = By.CssSelector("[aside-header]");
            CloseButton = By.CssSelector(".icons.close");
        }

        // we first retrieve by _driver
        public IWebElement GetAsideElement() => _driver.FindElement(AsideElement);

        // then for subsequent we use the app and locate elements inside
        // this also helps to drastically shorten our selectors
        public IWebElement GetAsideHeader() => GetAsideElement().FindElement(AsideHeader);

        public void Close() => GetAsideElement().FindElement(CloseButton).Click();

        // we can also Assert directly in the POM class - this can depend on different teams/projects
        // One thing I've found is that writing codes like that drastically helps with readability
        // e.g. AsideComponent.AssertIsDisplayed()
        public void AssertIsDisplayed() => Assert.IsTrue(GetAsideElement().Displayed);

        public void AssertUrlContainsView() => Assert.IsTrue(_driver.Url.Contains("aside", StringComparison.InvariantCultureIgnoreCase));

        public void AssertAsideHeaderIs(string header) => Assert.AreEqual(header, GetAsideHeader().Text);
    }