If test automation code becomes disorganized and difficult to maintain, Design Patterns can provide a solution. Coined by a group of IBM programmers known as the “Gang of Four,” Design Patterns are described in their book Design Patterns – Elements of Reusable Object-Oriented Software as a way of building customized objects and classes to solve specific software design problems. In essence, Design Patterns offer a standard approach to common issues. Although test automation is a form of software design, many engineers are not familiar with Design Patterns, which is unfortunate as they can offer significant benefits.
It’s important to clarify that Design Patterns are not algorithms and cannot be inserted into code like a library. Rather, they are high-level solution that guides the structure, relationships, and hierarchy of objects, classes, and interfaces in the application.
The majority of Design Patterns utilize the SOLID principles, which strive to produce software that is clear, legible, modifiable, expandable, and sustainable.
- Single Responsibility Principle asserts that a class should have only one responsibility.
- Open/Closed Principle dictates that entities should be open for extension but closed for modification.
- Liskov Substitution Principle specifies that a superclass may be substituted by a subclass without altering the behavior of the code.
- Interface Segregation Principle advises that a class should not rely on methods it does not need.
- Dependency Inversion Principle holds that high-level modules ought not to rely on low-level modules, and abstractions should not be dependent on specifics.
Builder Pattern
The purpose of the Builder Pattern is to isolate the creation of a complicated object from its depiction. This enables the same construction process to produce various representations.
The builder pattern can be used when:
- The process of constructing a complex object should be decoupled from the individual parts that constitute the object and its assembly.
- The process of building the object must be capable of generating multiple representations of the final product.
The issue that frequently arises in our tests is the reliance on a constructor, which can be resolved by utilizing the builder pattern.
Using the Builder Pattern has some gains:
- To increase test clarity, hide unnecessary details and only include relevant data for that particular test.
- Improving expressiveness: Tests become better documentation when we only pass necessary data explicitly. By looking at a test, one can understand what the method does.
- Enhancing flexibility: By decoupling the test from the constructor, we ensure that future changes won’t break existing tests. This is crucial for maintenance purposes as it prevents having to change many tests due to code changes.
- Ensuring reliability: When a test is flexible to changes, it does not need to be modified frequently, which increases its reliability.
Typically, an automated test becomes more reliable over time as it matures. To illustrate this point, consider the difference between a newly written automated test that fails and one that was written months ago. A newly written test that fails can have numerous causes, such as an error in the test or missing code. In contrast, a test that has been functioning for an extended period but suddenly fails is more alarming and suggests a problem with the new code.
Decorator Pattern
The Decorator Pattern aims to add new responsibilities to an object dynamically, without altering its original structure. Instead of extending functionality through subclassing, decorators offer a more flexible approach.
The decorator pattern should be used:
- To enhance the functionality of specific objects dynamically and transparently without impacting other objects
- For responsibilities that can be withdrawn
- When extension by subclassing is impractical
Sometimes, supporting every possible combination of independent extensions would lead to a large number of subclasses, resulting in an explosion of classes. Additionally, there may be situations where a class definition is not available for subclassing.
Strategy Pattern
The aim of the strategy pattern is to establish a group of algorithms, encapsulate each one, and make them replaceable. This implies that instead of implementing a single algorithm directly, the code receives instructions at runtime on which algorithm from the group to use.
By decoupling the algorithm from the client code that uses it, the strategy pattern enables the algorithm to vary independently, which postpones the decision on which algorithm to use until runtime. This approach enhances the flexibility and reusability of the calling code.
One scenario where the strategy pattern can be useful is in a user registration process during testing. For instance, there may be two different implementations of this action:
- The first implementation could involve the actual flow of transitions through the user interface for successful registration.
- The second implementation could be a brief API call that is invoked when a new user is needed for the test.
You may want to avoid invoking the time-consuming user registration via the UI every time in your tests. However, there may be instances when you need to validate the actual registration through the web, which requires a long registration process.
On the other hand, you need a fast and reliable way to create new users for testing purposes, which is where registration via REST API comes in handy.
You can use the Strategy pattern when:
- The Strategy pattern can be useful in several scenarios. If you have many related classes that differ mainly in their behavior, Strategies provide a way to configure a class with one of many behaviors. Additionally, when you need different variants of an algorithm, such as algorithms reflecting different space/time trade-offs, Strategies can be used to implement them as a class hierarchy of algorithms.
- If an algorithm uses data that clients shouldn’t know about, the Strategy pattern can help avoid exposing complex, algorithm-specific data structures. Finally, if a class has many behaviors that appear as multiple conditional statements in its operations, instead of using many conditionals, it is better to move related conditional branches into their own Strategy class.
Read more Automation Testing Solution in SHIFT ASIA – Automation seems like a one-fits-all solution, but it isn’t because what can be automated really depends on how the software is structured and how changes are pushed. It takes automation solution experts to determine feasibility, organize regression test suites and adopt the right tools.
How To Get Started?
After learning about SOLID principles, the advantages and disadvantages of design patterns, and seeing some examples of design patterns, you may wonder how to start using them effectively.
There are numerous design patterns, and it is unrealistic to expect to remember them all just by reading a book or attending a presentation. However, it is essential to understand that design patterns exist and can help solve common software design problems.
To begin, it is crucial to identify the problem you are trying to solve and assess whether SOLID and other programming principles are being violated. This knowledge can guide you in learning more about patterns that can address those issues.
A useful starting point is to review example code and evaluate the benefits and drawbacks of specific patterns you are considering. This approach can help you select the most suitable design pattern for your particular problem and effectively apply it to your codebase.
How To Not Use A Design Pattern
Using design patterns should not be done haphazardly. While they offer benefits such as flexibility and variability, they can also introduce extra levels of indirection, leading to performance costs and/or complex designs.
Therefore, design patterns should only be employed when their flexibility is truly necessary. It is important to assess the potential consequences in advance to understand both the advantages and drawbacks of a particular pattern.
Drawbacks & Limitations
Design patterns come with potential drawbacks that must be taken into account or at least considered:
- Design patterns do not necessarily result in direct code reuse.
- Patterns can sometimes appear deceptively simple and lead to incorrect implementations.
- Teams may suffer from pattern overload, where too many patterns are introduced and lead to confusion.
- Integrating patterns into a software development process can be a time-consuming and human-intensive activity.
Closing
Design patterns have the potential to greatly benefit most code bases, but their study and application can come with some overhead. It’s important to carefully consider which pattern to use and what advantages and disadvantages come with it.
For more information about design patterns and other techniques and practices to apply to your test automation code base, be sure to check out my upcoming book, “Automation Foundations (No fool with a tool).”