In the first article from the series, we discussed a strategy to create more maintainable and readable page object models in Selenium WebDriver tests. We used the Fluent Interface design pattern for achieving maximum API usability. Next, we talked about another API usability pattern called Singleton for creating only once page object models and afterward reusing them. Lastly, we discussed how the Template method pattern can help us fight issues related to non-interactable elements.
In this publication, we will focus on another common problem in automated tests – test workflows. The workflow is a series of actions and assertions that need to happen always in almost identical order. In our case with the search workflow – type a search term, apply many filters, open detailed results page, verify correct image information is displayed.
Test Case
We will automate the main Bing page. We will write logic for using the advanced images’ filtering options.
After that, we will open the detailed page about specific image results and verify that the correct title and URL are displayed.
Let’s quickly review how to tests looked without using any other patterns.
[TestClass] public class FacadeTests { private IWebDriver _driver; private MainPage _mainPage; private ResultDetailedPage _resultDetailedPage; [TestInitialize] public void SetupTest() { _driver = new ChromeDriver(); _mainPage = new MainPage(_driver); _resultDetailedPage = new ResultDetailedPage(_driver); } [TestCleanup] public void TeardownTest() { _driver.Quit(); } [TestMethod] public void AssertAtpSearchImageResults_NoFacade() { _mainPage .Open<MainPage>() .Search("automate the planet") .ClickImages() .SetSize(Sizes.Large) .SetColor(Colors.BlackWhite) .SetTypes(Types.Clipart) .SetPeople(People.All) .SetDate(Dates.PastYear) .SetLicense(Licenses.All) .ClickImageResult(1); _resultDetailedPage.AssertResultTitle("Homepage - Automate The Planet") .AssertResultLink("https://www.automatetheplanet.com/") .ClickVisitSiteButton(); Assert.AreEqual("https://www.automatetheplanet.com/", _driver.Url); } [TestMethod] public void AssertTPSearchImageResults_NoFacade() { _mainPage .Open<MainPage>() .Search("testproject.io") .ClickImages() .SetSize(Sizes.ExtraLarge) .SetColor(Colors.ColorOnly) .SetTypes(Types.Clipart) .SetPeople(People.All) .SetDate(Dates.PastWeek) .SetLicense(Licenses.All) .ClickImageResult(1); _resultDetailedPage.AssertResultTitle("TestProject · GitHub") .AssertResultLink("https://github.com/testproject-io") .ClickVisitSiteButton(); Assert.AreEqual("https://github.com/testproject-io", _driver.Url); } }
We have two separate tests with the exact same workflow, but we use different data. When we need to test a new search term or apply different filters, we will copy one of the existing tests and change the data. There is one huge drawback of such a solution. If the workflow changes, for example, we move the step with applying filters to the last page or add a new filter type, we need to go through the code and rearrange the steps for all tests.
We can solve the problem in a more elegant way through the usage of the Facade design pattern.
Facade Design Pattern
Definition:
Facades are classes that ease the use of a large chunk of dependent code. Usually, providing a simplified interface – a couple of methods with fewer parameters, instead of tons of initializations of objects. They hide the complexity of the underlying dependent components and expose only the methods that the user needs.
UML Class Diagram:
Participants:
The classes and objects participating in this pattern are:
- Facade Class – the main class that the user will use to access the simplified API. It usually contains public methods that use the dependent classes’ logic.
- Dependent Classes – they hold specific logic which is later used in the facade. They are usually used as parameters in the facade.
Facade Design Pattern Implementation
In our scenario, we can create a facade for performing searches. It will contain a single method called SearchImage that will call the pages’ methods in the right order.
public class ImageSearchFacade { private readonly MainPage _mainPage; private readonly ResultDetailedPage _resultDetailedPage; private readonly IWebDriver _driver; public ImageSearchFacade(IWebDriver driver, MainPage mainPage, ResultDetailedPage resultDetailedPage) { _driver = driver; _mainPage = mainPage; _resultDetailedPage = resultDetailedPage; } public void SearchImage(SearchData searchData, string expectedTitle, string expectedUrl) { _mainPage .Open<MainPage>() .Search(searchData.SearchTerm) .ClickImages() .SetSize(searchData.Size) .SetColor(searchData.Color) .SetTypes(searchData.Type) .SetPeople(searchData.People) .SetDate(searchData.Date) .SetLicense(searchData.License) .ClickImageResult(searchData.ResultNumber); _resultDetailedPage.AssertResultTitle(expectedTitle) .AssertResultLink(expectedUrl) .ClickVisitSiteButton(); Assert.AreEqual(expectedUrl, _driver.Url); } }
We use composition to hold the dependent pages. We initialize the page objects in the constructor. Also, all data required for the workflow is supplied in the form of facade methods’ parameters.
I grouped most of the parameters we need for the workflow in a single data class called SearchData.
NOTE: Composition over inheritance (or composite reuse principle) in object-oriented programming (OOP) is the principle where classes should achieve polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) rather than inheritance from a base or parent class. This is especially important in programming languages like C#, where multiple class inheritance is not allowed.
Also, we make assertions inside the function, which may seem that it breaks the Single Responsibility Principle. However, in this particular case, we define the test structure, which is the primary purpose of the method. A better name may be – VerifySearchImage or something similar. You may decide to move the assertion methods to separate facade methods. However, I prefer my facade methods to be more concise since we perform a few verifications after some of the actions, not just at the end of the test.
Shall we examine how the tests will look if we use the new facade? 🧐
[TestClass] public class FacadeTests { private IWebDriver _driver; private MainPage _mainPage; private ResultDetailedPage _resultDetailedPage; private ImageSearchFacade _imageSearchFacade; [TestInitialize] public void SetupTest() { _driver = new ChromeDriver(); _mainPage = new MainPage(_driver); _resultDetailedPage = new ResultDetailedPage(_driver); _imageSearchFacade = new ImageSearchFacade(_driver, _mainPage, _resultDetailedPage); } [TestCleanup] public void TeardownTest() { _driver.Quit(); } [TestMethod] public void AssertAtpSearchImageResults_WithFacade() { var searchData = new SearchData() { SearchTerm = "automate the planet", Size = Sizes.Large, Color = Colors.BlackWhite, Type = Types.Clipart, People = People.All, Date = Dates.PastYear, License = Licenses.All, ResultNumber = 1, }; _imageSearchFacade.SearchImage( searchData, "Homepage - Automate The Planet", "https://www.automatetheplanet.com/"); } [TestMethod] public void AssertTPSearchImageResults_WithFacade() { var searchData = new SearchData() { SearchTerm = "testproject.io", Size = Sizes.ExtraLarge, Color = Colors.ColorOnly, Type = Types.Clipart, People = People.All, Date = Dates.PastWeek, License = Licenses.All, ResultNumber = 1, }; _imageSearchFacade.SearchImage( searchData, "TestProject · GitHub", "https://github.com/testproject-io"); } }
As you can see, they became shorter and we were able to hide some of the low-level details. This may be a good or a bad thing. Some engineers may say that this made the readability a bit worse which may be right. If you want to see the exact workflow, now you need to open the facade class file. However, we improved the maintainability significantly since if we need to make a change in the workflow, we can do it in a single place. Another improvement is that the creation of new tests is more straightforward.
Combining Facade with Template Method Design Pattern
In the previous section, we saw how we could reuse the test workflows. However, there might be cases where this solution may not be sufficient. Imagine that we have the current search, yet we decide to redesign it after a year or two. The workflow will stay the same, but since it is possible to use another backend system, the way we apply filters or search items can slightly change.
We may need to support both searches for a while to decide which design suits our users better. If you recall the search facade’s code, you will notice that it depends on specific page objects. They will be different for the new search. At the same time, when we deprecate the old search, we want to change the existing tests as little as possible. How can we prepare for such events? We can combine the two design patterns discussed in this chapter – Template Method and Facade.
Image Search Facade with Template Methods
Instead of having a facade with specific dependent pages, we can create an abstract one that defines the workflow using template methods. Later we can create concrete implementations of the facade – one with the new and one with the old page objects.
public abstract class TemplateMethodFacade { public void SearchImage(SearchData searchData, string expectedTitle, string expectedUrl) { OpenSearchPage(); Search(searchData.SearchTerm); SetSize(searchData.Size); SetColor(searchData.Color); SetTypes(searchData.Type); SetPeople(searchData.People); SetDate(searchData.Date); SetLicense(searchData.License); OpenImageResults(searchData.ResultNumber); AssertResult(expectedTitle, expectedUrl); } protected abstract void OpenSearchPage(); protected abstract void Search(string searchTerm); protected abstract void SetSize(Sizes size); protected abstract void SetColor(Colors color); protected abstract void SetTypes(Types type); protected abstract void SetPeople(People people); protected abstract void SetDate(Dates date); protected abstract void SetLicense(Licenses license); protected abstract void OpenImageResults(int resultNumber); protected abstract void AssertResult(string expectedTitle, string expectedLink); }
Just like in the ImageSearchFacade, the workflow is defined in the public method SearchImage, but instead of calling the page objects’ methods, we call the abstract template methods.
Concrete Facade Implementation
For the old search and for the new one, we will have separate implementations of the above abstract TemplateMethodFacade. Let’s name the old search implementation ImageSearchFirstVersionFacade.
public class TemplateMethodSearchFacade : TemplateMethodFacade { private readonly MainPage _mainPage; private readonly ResultDetailedPage _resultDetailedPage; private readonly IWebDriver _driver; public TemplateMethodSearchFacade(IWebDriver driver, MainPage mainPage, ResultDetailedPage resultDetailedPage) { _driver = driver; _mainPage = mainPage; _resultDetailedPage = resultDetailedPage; } protected override void OpenSearchPage() { _mainPage.Open<MainPage>(); } protected override void Search(string searchTerm) { _mainPage.Search(searchTerm).ClickImages(); } protected override void SetSize(Sizes size) { _mainPage.SetSize(size); } protected override void SetColor(Colors color) { _mainPage.SetColor(color); } protected override void SetTypes(Types type) { _mainPage.SetTypes(type); } protected override void SetPeople(People people) { _mainPage.SetPeople(people); } protected override void SetDate(Dates date) { _mainPage.SetDate(date); } protected override void SetLicense(Licenses license) { _mainPage.SetLicense(license); } protected override void OpenImageResults(int resultNumber) { _mainPage.ClickImageResult(resultNumber); } protected override void AssertResult(string expectedTitle, string expectedUrl) { _resultDetailedPage.AssertResultTitle(expectedTitle) .AssertResultLink(expectedUrl) .ClickVisitSiteButton(); Assert.AreEqual(expectedUrl, _driver.Url); } }
As with the initial design of the pattern, the page objects were initialized in the constructor, and we used composition to store the pages needed for the implementation of our method. However, here we don’t define the workflow method SeachImage since we inherit it from the base class. Instead, we only need to override and implement all protected abstract methods, and this is where we call the logic from the dependent page objects. We can create a similar facade for the new search with its own page objects that will be used in the same workflow.
The usage in tests remains identical, we just use the concrete facade – ImageSearchFirstVersionFacade instead of ImageSearchFacade.
Summary
This article from the series discussed how we could reuse test workflows through the Facade design pattern. In the end, we combined both patterns, which allowed us to reuse the test workflow for different versions of the web pages. You can try it, and I believe it will make your tests much more maintainable, less flaky, and save you time creating new tests. However, keep in mind that the test understandability decreased a bit because we are hiding all of the details from the reader.
For a more detailed overview and usage of many more design patterns and best practices in automated testing, check my book “Design Patterns for High-Quality Automated Tests, C# Edition, High-Quality Tests Attributes, and Best Practices“. You can read part of three of the chapters here:
- Defining High-Quality Test Attributes for Automated Tests
- Benchmarking for Assessing Automated Test Components Performance
- Generic Repository Design Pattern- Test Data Preparation
Happy Testing! 🚀