C# PageFactory – Starting with Controllers

C# PageFactory – Starting with Controllers

This entry is part 3 of 5 in the series Building a .Net Core PageFactory Framework

Last time I explained why I use methods rather than properties in making calls to a Selenium WebDriver. This time I am going to start explaining how I go about writing maintainable code in PageController classes to interact with an HTML page through the WebDriver, and try to ground my decisions in the SOLID design prinicples.

Note I am calling them controllers rather than Page Objects. I feel that the classic PageObject violates the first of the SOLID design principles, the Single Responsibility Principle.

What is the Page Object Model?

The Selenium project itself is the source of truth on the original Page Object Model. The summary described it as follows:

  • The public methods represent the services that the page offers
  • Try not to expose the internals of the page
  • Generally don’t make assertions
  • Methods return other PageObjects
  • Need not represent an entire page
  • Different results for the same action are modelled as different methods

That is quite a lot to be going on within a single class!

Why Controllers not Page Objects?

The single responsibility principle

A class should have one, and only one, reason to change.

Robert C ‘Uncle Bob’ Martin http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

In 2014 Robert C Martin himself explained where his reasoning for this came from including this much earlier article:

“We have tried to demonstrate by these examples that it is almost always incorrect to begin the decomposition of a system into modules on the basis of a flowchart. We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.”

David L. Parnas On the Criteria To Be Used in Decomposing Systems into Modules. December 1972 issue of the Communications of the ACM, Volume 15, Number 12.

In this case, the elements displayed and the locators needed to find them are the things that are likely to change as well as the url and page title.

One of the key design decisions made here is the means of interacting with the system under test:

  • Selenium WebDriver to interact with a Desktop Browser
  • Appium to interact with a mobile application
  • A N other technology completely

In reality, the rest of my test system has no need to know how the interaction happens, it just needs a way to interact with the individual web pages. The technology with which it does that is an implementation detail. What it does need is a class that can provide information from the browser (text, link details etc) as well as interact with its elements.

This is what I mean by a controller. Other details such as where I end up next can be handled somewhere else in my code.

PageControllers and BlockControllers

Look at this page. Some aspects of it are very specific to this precise page:

  • its url
  • the (page tab) title

Other parts of the page are generated from objects that are used around the site:

  • header
  • footer
  • some widgets
  • the article panel shares its structure with other blog posts
  • a navigation section

Following the DRY (Don’t Repeat Yourself) principle I don’t want to be writing the same code for these items everywhere that I might use them.

My preference is to use an object call a PageController for the specific details (url and title) and objects known as BlockControllers for anything that has a root element inside the <body> element of the DOM.

Rather than generating a large interface for all of the interactions on a particular web page, it is a good time to use the I from SOLID, the interface segregation principle. In this case I started out by defining a simple interface for a page (defined by me as a single url) in this case through an Abstract class called Page.

Abstract Page class (first try)

using OpenQA.Selenium;

namespace AlexanderOnTest.NewNetPageFactory
{
    public abstract class Page
    {
        private readonly IWebDriver driver;

        protected Page(IWebDriver driver)
        {
            this.driver = driver;
        }

        public abstract string GetExpectedPageTitle();

        public abstract string GetExpectedUri();

        public string GetActualPageTitle()
        {
            return driver.Title;
        }

        public string GetActualUri()
        {
            return driver.Url;
        }
    }
}

Notice that there are two aspects here. I provide reusable implementations for the controller actions, but leave it to the implementor to provide the expected data.

The interface segregation principle

“Clients should not be forced to depend upon interfaces that they do not use.”

Robert C ‘Uncle Bob’ Martin 1996 https://drive.google.com/file/d/0BwhCYaYDn8EgOTViYjJhYzMtMzYxMC00MzFjLWJjMzYtOGJiMDc5N2JkYmJi/view

Looking at this immediately makes clear that there are two things going on here, expected data and returned data. Perhaps they should be in separate classes? In reality I would prefer to move the expected data out to a higher level.

I am not prepared to make that change right now; but by separating the interfaces I can make the separation much easier to make later.

Let’s make two interfaces IPageController and IPageData.

IPageData interface

namespace AlexanderOnTest.NewNetPageFactory
{
    public interface IPageData
    {
        string GetExpectedPageTitle();

        string GetExpectedUri();
    }
}

IPageController interface

using System;

namespace AlexanderOnTest.NewNetPageFactory
{
    public interface IPageController
    {
        string GetActualPageTitle();

        string GetActualUri();
    }
}

This means that I can implement a generic PageController that can be used directly for any page, or inherited to add e.g. the IPageData methods for a particular page.

Reusable PageController class

using OpenQA.Selenium;

namespace AlexanderOnTest.NewNetPageFactory
{
    public class PageController : IPageController
    {
        private readonly IWebDriver driver;
        
        public PageController(IWebDriver driver)
        {
            this.driver = driver;
        }

        public string GetActualPageTitle()
        {
            return driver.Title;
        }

        public string GetActualUri()
        {
            return driver.Url;
        }
    }
}

Final abstract Page class

using OpenQA.Selenium;

namespace AlexanderOnTest.NewNetPageFactory
{
    public abstract class Page : PageController, IPageData
    {
        protected Page(IWebDriver driver) : base (driver) { }

        public abstract string GetExpectedPageTitle();

        public abstract string GetExpectedUri();
    }
}

For now my Page still has responsibility for returning expected and actual data about the url of my web page, but I can also just chose to use the smaller IPageController interface (and PageController implementaion) if I don’t need the expected data methods.

That will do for now. I will look at creating BlockController objects in the next article in this series.

Progress made:

  • Starting to use SOLID principles to develop my ‘PageFactory’ code
  • Starting the abstraction for my testing, I have so far created interfaces for expected (IPageData) and actual (IPageController) url and page title values.
  • An abstract Page object that ‘implements’ both
  • A full PageController implementation

Lessons learnt:

  • The single responsibility principle encourages small classes for one purpose.
  • The interface segregation principle encourages small logical interfaces so that implementations don’t have to support unnecessary methods. A class can implement many interfaces.

A reminder:

If you want to ask me a question, Twitter is undoubtedly the fastest place to get a response: My Username is @AlexanderOnTest so I am easy to find. My DMs are always open for questions, and I publicise my new blog posts there too.

Series Navigation<< C# PageFactory – Why I don’t use properties.C# PageFactory – Wrap your WebDriver calls >>

One thought on “C# PageFactory – Starting with Controllers

Comments are closed.

Comments are closed.