Design Patterns can help, when test automation code becomes messy and difficult to maintain. The term “Design Patterns” was created by four IBM programmers, nicknamed and often referred to as the “Gang of Four” (GoF). In their 1994 book Design Patterns - Elements of Reusable Object-Oriented Software, they define a Design Pattern as “a description of customized communicating objects and classes that solves a problem in a particular context of software design”. Put more simply “a Design Pattern is a common way of building things that solves a known problem”. Test automation is software design, yet many test automation engineers are unaware of many -or any- design patterns that could ease their work. This is a pity because using design patterns has quite some advantages.
To avoid misunderstandings, it also needs to be clarified, what Design Patterns are not: Design patterns are not algorithms. They can’t be copied and pasted into existing code like a library, because they are not a specific piece of code. Design patterns are more a high-level description of a solution, that is meant to provide guidance on the structure, relations, and hierarchy of an object, as well as classes and interfaces in the application.
Most design patterns make use of the SOLID principles, which aim to create software that is understandable, readable, changeable, extensible, and maintainable:
- Single Responsibility states that a class should be responsible for only one thing
- Open/ Closed means that entities should be open for extension, but closed for modification
- Liskov Substitution: A superclass can be replaced by a subclass without changing the code’s behavior
- Interface Segregation: A class shouldn’t depend on methods it doesn’t use
- Dependency Inversion states that high-level modules should not import anything from low-level modules, and abstractions should not depend on details
The objective of the Builder Pattern is to separate the construction of a complex object from its representation so that the same construction action can create different representations.
The builder pattern can be used when:
- The algorithm for creating a complex object should be independent of the parts that make up the object, and how they're assembled.
- The construction process must allow different representations for the object that's constructed.
The problem we’re often having in our tests is that the tests are bound to a constructor, and the builder pattern can resolve that dependency.
Using the Builder Pattern has some gains:
- Hide the details that are not important to the test, so that only relevant data for that specific test is present.
- Expressiveness: By explicitly passing just the necessary data we improve the value of our tests as a form of documentation. Just by looking at the test, you can understand what the method does.
- Flexibility: By decoupling the test from the constructor, we made sure that future changes won't break our existing tests. This is important for maintenance reasons, e.g. you don't want to go in and change a lot of tests, because of some code changes.
- Reliability: Because our test is flexible against changes, you won't have to modify it often.
In general, an automated test gets more reliable when it matures. To imagine this, you can compare the effect of a failing automated test that you just wrote, with one that was written months ago: A newly written test that fails can have a lot of causes: e.g. an error in the test, some missing code, etc. On the other hand, a test that has been working for a long time but suddenly fails is more concerning and is most definitely indicating a problem with new code.
The intent of the decorator pattern is to attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
The decorator pattern should be used:
- To add responsibilities to individual objects dynamically and transparently, that is, without affecting other objects
- For responsibilities that can be withdrawn
- When extension by subclassing is impractical
Sometimes a large number of independent extensions are possible and would produce an explosion of subclasses to support every combination. Or a class definition may be hidden or otherwise unavailable for subclassing.
The strategy pattern’s intent is to define a family of algorithms, encapsulate each one, and make them interchangeable. This means, instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use. Strategy lets the algorithm vary independently from clients that use it, deferring the decision about which algorithm to use until runtime allows the calling code to be more flexible and reusable.
One example could be a user registration you perform in your tests. For user registration, you might want to have two different implementations of this particular action.
- The first one would be the actual flow of transitions through the UI for successful user registration
- The other one would be a short API call, which is invoked when a new user is needed for the test
You may not want to invoke the “long” registration via the UI every time in your tests. But sometimes you’ll need it, for example when you want to validate the actual registration through the web. And vice versa, we need the new-user-creation for tests to be fast and reliable. That’s why registration via REST API would be suitable here.
You can use the Strategy pattern when:
- Many related classes differ only in their behavior. Strategies provide a way to configure a class with one of many behaviors.
- You need different variants of an algorithm. For example, you might define algorithms reflecting different space/time trade-offs. Strategies can be used when these variants are implemented as a class hierarchy of algorithms.
- An algorithm uses data that clients shouldn't know about. Use the Strategy pattern to avoid exposing complex, algorithm-specific data structures.
- A class defines many behaviors, which appear as multiple conditional statements its operations. Instead of many conditionals, move related conditional branches into their own Strategy class.
How To Get Started?
So having heard about the SOLID principles, advantages, and disadvantages and of design patterns, as well as having seen some examples of design patterns, how to get started using them?
There are a lot of design patterns existing, and we most likely cannot remember them all just after reading a book or having heard a presentation about them.
The most important thing is to know that design patterns exist.
Second important thing is to know what problem we are trying to solve. This with the help of knowing about SOLID and other programming principles and if they are violated. We then can learn more about patterns that are trying to solve those problems.
A good start for this can be to look at example code and also to look at the benefits and drawbacks of specific patterns you’re evaluating.
How To Not Use A Design Pattern
Design patterns should not be used aimlessly. Very often, the advantages like flexibility and variability cause additional levels of indirection to be introduced, which can cost performance, and/ or complicate the design.
Only when the provided flexibility of a design pattern is really needed, the pattern should be used. Checking out the consequences upfront can be very helpful to figure out a pattern's advantages and liabilities.
Drawbacks & Limitations
Using design patterns can also have negative implications, which need to be taken care of, or at least to be considered:
- Patterns do not lead to direct code reuse
- Patterns can be deceptively simple
- Teams may suffer from pattern overload
- Integrating patterns into a software development process is a human-intensive activity
Design patterns can be a great benefit for most code bases, but studying and applying them can cause some overhead, so you need to carefully decide which pattern to use, and what advantages and disadvantages can come with it.
You can read more about design patterns, and other techniques and practices to apply to your test automation code base in my upcoming book: Automation Foundations (No fool with a tool).
About Christian Baumann
Christian is a principal software tester with 15+ years of experience in the field of software testing. He has successfully held different roles in the context of testing from Test Automation Engineer to Test Team Lead. During his career he worked with various test automation tools using programming languages, but also applied certain development testing methodologies. Christian is strongly driven by his context, always searching for the best fitting solution for a given situation. He's able to understand business´ and people’s problems, and is always eager to learn and improve himself, while staying curios, open minded and willing to share his knowledge.