Undoubtedly, Page Objects represent the predominant approach in test automation design for modeling web pages into framework classes. Despite some misconceptions within the testing community regarding its relevance, I'll demonstrate how this concept remains valuable and aligned with contemporary coding practices.
๐จ Definition
The Page Object Model aims to map HTML pages into classes within your test automation framework. By encapsulating web page elements in code, it enhances scalability, readability, and productivity, thereby reducing maintenance costs.
The concept of a page object is that it should replicate your web pages accordingly to how they are displayed in your application. As usual, I recommend you to learn it deeper. For now, let's go straight to the business and see how THE CLEVER TESTER SOLUTION looks like.
๐ ๏ธ Service Classes
If you remember the example we used in the last post...
[Given(@"I add (.*) to my shopping cart")]
public void GivenIAddItemToCart(string myItem){
// an injected facade class object
_shoppingCartService
.SelectItem(myItem) //smaller domain rules
.SelectQuantity()
.AddToCart()
;
}
We have the "ShoppingCartService
" facade class, which is our first service (business) layer class:
public ShoppingCartService SelectItem(string itemName) {
_productsPage.SelectItem(itemName);
return this;
}
public ShoppingCartService SelectQuantity()
{
_productsPageWrapper
.SelectQuantity()
;
return this;
}
In our example, the "ShoppingCartService
" facade class serves as the initial service layer. It utilizes methods that interact with Page Objects (in this case, products page), including a wrapper implementing the Decorator pattern to extend functionality - adding an extra behavior to the original page object.
๐ ๏ธ Page Object Class
Now, let's see how the page object class looks like:
public class ProductsPage : ProductsPageInterface
{
public string ItemLocator(string itemId) => $"//*[@id='{itemId}']";
public string AddItemQuantityLocator = "//*[contains(text(),'plus')]";
public string AddItemConfirmButtonLocator = "//*[@id='confirm']";
private readonly ProductsPageMapper _productsPageMapper;
public ProductsPage( ProductsPageMapper productsPageMapper)
{
_productsPageMapper = productsPageMapper;
}
public ProductsPageInterface SelectItem(string itemId)
{
_productsPageMapper.ClickOnElement(ItemLocator(itemId));
return this;
}
public ProductsPageInterface SelectQuantity()
{
_productsPageMapper.ClickOnElement(AddItemQuantityLocator);
return this;
}
//...
}
The main Page Object class, "ProductsPage
," instantiates a mapper responsible for accessing web elements. It adheres to principles such as contextual modeling, logic-free methods for simplicity and reusability, and interfaces for flexible design. Note that the mapper is written in generic terms as it sits deeper in the overall automation solution then the classes we were exploring before. We start to provide higher reusability from this layer onwards, as we get far from the application domain (business) at this point and focuses more in the implementation (repository).
Several key points are worth noting:
The code in the page class should model the application context.
The available methods should avoid incorporating programming logic to minimize complexity and enhance reusability. Each function should simply interact with the web page. If additional complexity is necessary, a wrapper class can be employed to maintain highly reusable, flexible, and maintainable code (an example will be demonstrated shortly).
Utilizing interfaces to adhere to the Decorator design pattern enhances flexibility while facilitating a fluent design.
๐ ๏ธ Decorator Class
Decorators, like our generic "ProductsPageConcreteDecorator,
" augment the original page class with additional behaviors. This approach ensures code reuse and transparency for service classes, maintaining separation of concerns:
//concrete decorator classes
//add extra behaviors such as wait conditions,
//or element mapper logic, and so on
public class ProductsPageConcreteDecorator : ProductsPageBaseDecorator
{
public ProductsPageConcreteDecorator(ProductsPageInterface productsPageInterface) : base(productsPageInterface)
{
}
//adds extra behavior to SelectQuantity
public override ProductsPageInterface SelectQuantity()
{
//add code for special conditions
return base.SelectQuantity();
}
//...
}
Consider a scenario where you require a "select quantity" function that performs bulk selection. It may not be desirable to expose this functionality in the page class, as doing so would create an anti-pattern, as mentioned earlier. In such cases, the decorator class can be employed. This approach enables the reuse of the original code (specifically, the "atomic" quantity selection provided in the page class), while the service class seamlessly interacts with this augmentation. Importantly, the service class does not need to comprehend the underlying logic, as it is handled entirely by the decorator and remains transparent to the business (service class).
๐ Conclusion
In conclusion, the Page Object Model serves as a robust baseline for structuring web automation test frameworks, offering benefits such as enhanced scalability, readability, and maintenance efficiency. Throughout our exploration, we've emphasized the importance of aligning with contemporary coding practices, such as encapsulating application context within page classes and utilizing decorators for extending functionality without compromising code integrity. By adhering to these principles, testing automation frameworks can achieve greater flexibility and adaptability, ultimately leading to more effective and sustainable testing efforts. As the landscape of software development continues to evolve, embracing these foundational concepts ensures that testing endeavors remain resilient.