After analyzing a series of design patterns applicable in automation testing, in this tutorial, we will classify them based on their utility while building a new framework. Their implementation is covered in Python 3, but of course, their benefits are the same in any other programming language you follow.
To recap, we’ve covered the following patterns:
- Page object pattern
- Singleton – with multiple implementations covered in two articles
- Factory pattern
- State machine
We will provide a short description of pros and cons, so in the end to classify them.
Page object pattern
This is the most popular design pattern in software automation. On brief, you separate the test case logic, from objects definition (every UI element from a product view will be encapsulated in the object, including ids, actions or other information)
- Maintainable and reusable code, having as a result: clean test cases.
On each page/object, we will gather a lot of information from a product, but what if?
- there is a lot of similarity between many pages? It leads to code duplication
- there is a huge quantity of information on a page? It leads to a code hard to follow
It restricts the instantiation of a class to a single object. An important usage in software automation is related to driver instantiation. e.g. Selenium WebDriver.
We cannot instantiate the driver multiple times, as a result:
- test execution time will be faster
- prevent multiple connections with the driver
The only disadvantage noticed is related to cross-platform testing. For example, if we want to run the same test on multiple web browsers, we need to instantiate the ‘webdriver’ multiple times, but we’ve already marked the ‘webdriver’ instantiation as a singleton so the test execution will not be possible.
This pattern states in creating objects based on some specific rules, but let subclasses to decide which class to instantiate. This idea could be easily translated in terms of software testing like:
“We will create objects for different UI blocks (like a page from Page Object Pattern) based on specific rules and let subclasses (the tests) to decide which classes to be instantiated (which actions to build our test)”
- It will solve the page object pattern problem of having a huge quantity of code into a single object, creating well organized and easy to maintain components (sub-pages)
- Implementation logic: writing or debugging tests easier is much easier.
- The code somehow could be more difficult to read, due to the abstraction introduced by restricting the object creation based on some specific rules.
- It’s great for large application, but for simple tests, there is too much logic to be added.
It is used to separate the construction of an object from its representation so the same construction flow can provide a different implementation. From tests perspective, each test could have the same steps, but their implementation could be different, leading to well-structured tests, knowing exactly where it actually failed.
- Tests written based on builder pattern could be easy to read and debug
- We don’t have to care about the implementation logic while writing tests so that even a non-tech person could understand the test flow
- Implementation logic is very structured and easy to adapt in case of feature changes
- Sometimes it’s hard to adapt each test to follow a specific implementation flow, leading to unnecessary code
- It’s more like a niche pattern specific to a flow-based validation, where we want to follow some clear steps for each implementation.
- Due to the constraint of having some clear steps, it could also lead to code duplication
It is part of the structural design patterns and used when we desire to expose a simplified interface towards the client that hides the complexity of a system. The structure is based on a single class which provides the users with sufficient subclasses and APIs needed to access the system below.
- Allows you to hide the complex definitions of your application and only expose a simple entry point.
- In automation, it is useful due to its readiness allowing to understand and implement a test case with ease.
- Also in automation, it allows new test cases to be quickly implemented.
- It can lead to a large number of subclasses and to code blocks duplication.
- The user has no control over the logic used. If changes need to be done, it implies a lot of effort.
This design pattern falls under the behavioral category and is used when we want to change the behavior of a class based on a given state. We create different objects for different states and inject them into a context class.
- Removes the necessity of if and switch statements, allowing transitions to be more compartmentalized and independently defined.
- Definition of some clear steps to be followed during execution.
- If the system is complex and requires a large number of states, it will lead to a large amount of code written. Each state has to be represented separately and can lead to code duplication.
Listed as a behavioral design pattern, the observer is used when we have a one-to-many relationship between objects. It is used when objects have to communicate between them and be notified when certain actions take place. For this, on observer acts a central hub for gathering and sharing information to other classes.
- Useful in automation when you want the test case to be notified of events that occur when testing a complex flow.
- Removes the necessity of hardcoded “sleep” statements.
- The incorrect use of the design pattern will add complexity to the implementation.
- The notification messages don’t respect an order, thus must be interpreted by the observer.
Another behavioral design pattern, the state machine is related to the state design pattern and shares in its logic of implementation, but with a key difference: the transition between states is predefined and managed by a “machine” class.
- Able to define connections between the states.
- Reduce the need to constantly inject new states.
- Difficulty when applying the pattern in mid to last stages of development (e.g. this should be followed straight from the beginning of the development, otherwise the effort required is unproductive).
In the end, we’ve made our top of design patterns applicable in software automation.
|1||Facade||This should be the entry point for any automation framework, like an interface|
|2||Page object||Grouping screens based on functionality it’s always a good idea (mostly used in client automation)|
|3||Factory||Works hand in hand with page object, providing a a faster and more organised access to the pages|
|4||Singleton||It’s complementary to any pattern, usefull for memory optimization|
|5||Observer||Best case is when the testing involves listeners|
|7||Builder||An alternative to Factory pattern, but with more restrictions|
|8||State machine||It could be the first step to make the transition from classical approach to the AI (artificial intelligance) one|
|9||State||A good pattern if the execution steps are well defined, but pretty difficult to adapt all the test cases to it|
To conclude, no matter what patterns you prefer, before starting a new framework, take a breath, analyze what you want to achieve and based on the product you want to test, analyze some patterns and create your framework based on them. My suggestion, start with a Facade as an entry point, afterward use Page object pattern but adapt it based on a factory. Always consider a singleton if you need to set a connection with a driver or a database. In the end depends on your app to be tested, consider to integrate some behavioral patterns.
More details about the patterns implementation could be found here