In the article Strategy Design Pattern, I explained the benefits of the Strategy Design Pattern application in your automation tests. Some of the advantages are more maintainable code, encapsulated algorithm logic, easily interchangeable algorithms, and less complicated code. The Strategy Design Pattern follows the Open-Closed Principle that states that “Classes should be open for extension, but closed for modification“. Another way to create open for extension classes is through the usage of the Decorator Design Pattern. In this publication, I will refactor the code examples from the previously mentioned articles to be even more extendable. The used strategies are going to be “wrapped” through decorators. The Decorator Design Pattern allows us easily to attach additional responsibilities to an object dynamically. I believe that it can be heavily utilized in automation tests because of all its benefits.
Note
If you are not familiar with the above patterns, I suggest you read my articles about them first to understand the presented concepts thoroughly. (Especially the ones related to Strategy Design Pattern*).*
Definition
The Decorator Design Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
Benefits
-
You can wrap a component with any number of decorators.
-
Change the component’s behavior by adding new functionality before and/or after method calls to the component.
-
Decorator classes mirror the type of components they decorate.
-
Provides an alternative to subclassing for extending behavior.
Abstract UML Class Diagram
classDiagram
Component <|-- ConcreteComponent
Component <|-- Decorator
Decorator <|-- ConcreteDecoratorA
Decorator <|-- ConcreteDecoratorB
Decorator o-- Component
class Component {
+methodA()
+methodB()
}
class ConcreteComponent {
+methodA()
+methodB()
}
class Decorator {
+componentWrappedObj
+methodA()
+methodB()
}
class ConcreteDecoratorA {
+newState
+addNewBehavior()
+methodA()
+methodB()
}
class ConcreteDecoratorB {
+newState
+addNewBehavior()
+methodA()
+methodB()
}
The classes and objects participating in this pattern are:
-
Component
Defines the interface for objects that can have responsibilities added to them dynamically.
-
Decorator
The decorators implement the same interface (abstract class) as the component they will decorate. The decorator has a HAS-A relationship with the extending object, which means that the former has an instance variable that references the latter.
-
ConcreteComponent
This is the object that is going to be enhanced dynamically. It inherits the Component.
-
ConcreteDecorator
Decorators can enhance the state of the component. They can add new methods. The new behavior is typically added before or after an existing method in the component.
Decorator Design Pattern Java Code
Test’s Test Case
The test case of the examples is going to be the same as the previous articles. The primary goal is going to be to purchase different items from Online Store. The prices on the last step of the buying process should be validated – taxes, discounts, etc.
1. Navigate to Item’s Page


4. Fill in the purchase info


The previous article explains in detail how to automate the whole purchase process. However, to introduce the Decorator Design Pattern’s benefits, only the last step is going to be necessary – Order Summary Validation. In the Strategy Design Pattern posts, the prices on the last stage of the purchase process are validated through different Purchase Strategies that implement the OrderPurchaseStrategy.
public class PurchaseContext {
private final OrderPurchaseStrategy orderPurchaseStrategy;
private final ItemPage itemPage;
private final ShoppingCartPage shoppingCartPage;
private final CheckoutPage checkoutPage;
public PurchaseContext(OrderPurchaseStrategy orderPurchaseStrategy) {
this.orderPurchaseStrategy = orderPurchaseStrategy;
itemPage = new ItemPage();
shoppingCartPage = new ShoppingCartPage();
checkoutPage = new CheckoutPage();
}
public void purchaseItem(
String itemUrl,
double itemPrice,
PurchaseInfo purchaseInfo
) {
itemPage.navigate(itemUrl);
itemPage.clickBuyNowButton();
itemPage.clickViewShoppingCartButton();
shoppingCartPage.clickProceedToCheckoutButton();
checkoutPage.fillBillingInfo(purchaseInfo);
orderPurchaseStrategy.assertOrderSummary(itemPrice, purchaseInfo);
}
}
A while ago, when we worked on the first version of the BELLATRIX test automation framework, I did this research. Afterward, we used a similar approach in many of the features of the solution.
Improved Version Advanced Strategy Design Pattern Applied
public class PurchaseContext {
private final OrderPurchaseStrategy[] orderPurchaseStrategies;
private final ItemPage itemPage;
private final ShoppingCartPage shoppingCartPage;
private final CheckoutPage checkoutPage;
public PurchaseContext(OrderPurchaseStrategy... orderPurchaseStrategies) {
this.orderPurchaseStrategies = orderPurchaseStrategies;
itemPage = new ItemPage();
shoppingCartPage = new ShoppingCartPage();
checkoutPage = new CheckoutPage();
}
public void purchaseItem(
String itemUrl,
double itemPrice,
PurchaseInfo purchaseInfo
) {
validatePurchaseInfo(purchaseInfo);
itemPage.navigate(itemUrl);
itemPage.clickBuyNowButton();
itemPage.clickViewShoppingCartButton();
shoppingCartPage.clickProceedToCheckoutButton();
checkoutPage.fillBillingInfo(purchaseInfo);
validateOrderSummary(itemPrice, purchaseInfo);
}
public void validatePurchaseInfo(PurchaseInfo purchaseInfo) {
for (var currentStrategy : orderPurchaseStrategies) {
currentStrategy.validatePurchaseInfo(purchaseInfo);
}
}
public void validateOrderSummary(
double itemPrice,
PurchaseInfo purchaseInfo
) {
for (var currentStrategy : orderPurchaseStrategies) {
currentStrategy.assertOrderSummary(itemPrice, purchaseInfo);
}
}
}
The usage of the PurchaseContext is not so straightforward, as you can see from the code below.
new PurchaseContext(new VatTaxOrderPurchaseStrategy(), new CouponCodeOrderPurchaseStrategy())
.purchaseItem(itemUrl, itemPrice, purchaseInfo);
Different prices validations mix is achieved through the iteration of the initialized strategies. However, the disadvantage of the provided solution is that for every new method in the OrderPurchaseStrategy interface, you need to create a new one with a “foreach” statement in the PurchaseContext class. Also, I believe that the initialization of the PurchaseContext in the test method is a little bit unreadable.
If you don’t understand the above code examples thoroughly, you can find more detailed explanations in my articles about the Strategy Design Pattern.
One of the resolutions of the initialization problem of the PurchaseContext is to create more strategy classes that combine the different behaviors, e.g., VatTaxOrderPurchaseStrategy, GiftOrderPurchaseStrategy, CouponCodeOrderPurchaseStrategy, NoTaxesOrderPurchaseStrategy, etc. But as you can see this escalated quickly – a typical example of a class explosion.
classDiagram
OrderPurchaseStrategy <|-- CouponCodeVatTaxOrderPurchaseStrategy
OrderPurchaseStrategy <|-- CouponCodeGiftOrderPurchaseStrategy
OrderPurchaseStrategy <|-- VatSalesTaxOrderPurchaseStrategy
OrderPurchaseStrategy <|-- SalesTaxOrderPurchaseStrategy
OrderPurchaseStrategy <|-- VatTaxOrderPurchaseStrategy
OrderPurchaseStrategy <|-- NoTaxOrderPurchaseStrategy
OrderPurchaseStrategy <|-- CouponCodeOrderPurchaseStrategy
OrderPurchaseStrategy <|-- GiftOrderPurchaseStrategy
OrderPurchaseStrategy <|-- CouponCodeVatTaxGiftOrderPurchaseStrategy
class OrderPurchaseStrategy {
+calculateTotalPrice()
+assertOrderSummary()
}
class CouponCodeVatTaxOrderPurchaseStrategy {
+vatTax
+vatTaxCalculationService
+couponCodeDiscount
+couponCodeCalculationService
+calculateTotalPrice()
+assertOrderSummary()
}
class CouponCodeGiftOrderPurchaseStrategy {
+giftWrappingPrice
+giftWrappingPriceCalculationService
+couponCodeDiscount
+couponCodeCalculationService
+calculateTotalPrice()
+assertOrderSummary()
}
class VatSalesTaxOrderPurchaseStrategy {
+vatTax
+vatTaxCalculationService
+salesTaxCalculationService
+calculateTotalPrice()
+assertOrderSummary()
}
class SalesTaxOrderPurchaseStrategy {
+salesTax
+salesTaxCalculationService
+calculateTotalPrice()
+assertOrderSummary()
}
class VatTaxOrderPurchaseStrategy {
+vatTax
+vatTaxCalculationService
+calculateTotalPrice()
+assertOrderSummary()
}
class NoTaxOrderPurchaseStrategy {
+assertOrderSummary()
}
class CouponCodeOrderPurchaseStrategy {
+couponCodeDiscount
+couponCodeCalculationService
+assertOrderSummary()
}
class GiftOrderPurchaseStrategy {
+giftWrappingPrice
+giftWrappingCalculationService
+calculateTotalPrice()
+assertOrderSummary()
}
class CouponCodeVatTaxGiftOrderPurchaseStrategy {
+vatTax
+giftWrappingPrice
+couponCodeDiscount
+couponCodeCalculationService
+calculateTotalPrice()
+assertOrderSummary()
}
If you need to add additional assertions, you will have to add a couple of more classes to achieve the mixing behavior. Here is where the Decorator Design Pattern comes to play. The attached behavior through inheritance can be determined only statically at compile time. However, through the help of composition, the decorators can extend the component at runtime.
Specific UML Class Diagram
classDiagram
OrderPurchaseStrategy <|-- TotalPriceOrderPurchaseStrategy
OrderPurchaseStrategy <|-- OrderPurchaseStrategyDecorator
OrderPurchaseStrategyDecorator <|-- NoTaxOrderPurchaseStrategy
OrderPurchaseStrategyDecorator <|-- GiftOrderPurchaseStrategy
OrderPurchaseStrategyDecorator <|-- VatTaxOrderPurchaseStrategy
OrderPurchaseStrategyDecorator <|-- SalesTaxOrderPurchaseStrategy
PurchaseContext --> OrderPurchaseStrategy
class PurchaseContext {
+orderPurchaseStrategy
+purchaseItem(String itemUrl, double itemPrice, PurchaseInfo purchaseInfo)
}
class OrderPurchaseStrategy {
+calculateTotalPrice()
+assertOrderSummary(double totalPrice)
}
class TotalPriceOrderPurchaseStrategy {
+calculateTotalPrice()
+assertOrderSummary(double totalPrice)
}
class OrderPurchaseStrategyDecorator {
+purchaseInfo
+itemPrice
+orderPurchaseStrategy
+calculateTotalPrice()
+assertOrderSummary(double totalPrice)
}
class NoTaxOrderPurchaseStrategy {
+assertOrderSummary(double totalPrice)
}
class GiftOrderPurchaseStrategy {
+giftWrappingPrice
+giftWrappingCalculationService
+calculateTotalPrice()
+assertOrderSummary(double totalPrice)
}
class VatTaxOrderPurchaseStrategy {
+vatTax
+vatTaxCalculationService
+calculateTotalPrice()
+assertOrderSummary(double totalPrice)
}
class SalesTaxOrderPurchaseStrategy {
+salesTax
+salesTaxCalculationService
+calculateTotalPrice()
+assertOrderSummary(double totalPrice)
}
Participants
The classes and objects participating in this pattern are:
-
OrderPurchaseStrategy (Component)
Defines the interface for all concrete strategies that will assert the different prices on the last step of the purchasing process.
-
OrderPurchaseStrategyDecorator (Component Decorator)
The decorator has an instance variable that holds a reference to the OrderPurchaseStrategy. Also, it contains another useful info that is going to be used by the concrete decorators to calculate the different expected amounts.
-
TotalPriceOrderPurchaseStrategy (ConcreteComponent)
It is a descendant of the OrderPurchaseStrategy, and it is used to verify the total cost of the order.
-
VatTaxOrderPurchaseStrategy (ConcreteDecorator)
Can extend the concrete order purchase strategies. Adds a new logic for validating the order’s VAT Tax and adds the new tax to the total price.
Refactoring Purchase Strategies to Support Decorator Design Pattern
The base class for all concrete strategies and their decorators is the OrderPurchaseStrategy.
public abstract class OrderPurchaseStrategy {
public abstract double calculateTotalPrice();
public abstract void assertOrderSummary(double totalPrice);
}
public class TotalPriceOrderPurchaseStrategy extends OrderPurchaseStrategy {
private final double itemsPrice;
public TotalPriceOrderPurchaseStrategy(double itemsPrice) {
this.itemsPrice = itemsPrice;
}
@Override
public double calculateTotalPrice() {
return itemsPrice;
}
@Override
public void assertOrderSummary(double totalPrice) {
var checkoutPage = new CheckoutPage();
Driver.waitForAjax();
Driver.waitUntilPageLoadsCompletely();
checkoutPage.assertions().assertOrderTotalPrice(totalPrice);
}
}
To be able to add a new behavior at runtime dynamically, all decorators need to derive from the class OrderPurchaseStrategyDecorator.
public class OrderPurchaseStrategyDecorator extends OrderPurchaseStrategy {
protected final OrderPurchaseStrategy orderPurchaseStrategy;
protected final PurchaseInfo purchaseInfo;
protected final double itemPrice;
public OrderPurchaseStrategyDecorator(
OrderPurchaseStrategy orderPurchaseStrategy,
double itemPrice,
PurchaseInfo purchaseInfo
) {
this.orderPurchaseStrategy = orderPurchaseStrategy;
this.itemPrice = itemPrice;
this.purchaseInfo = purchaseInfo;
}
@Override
public double calculateTotalPrice() {
validateOrderStrategy();
return orderPurchaseStrategy.calculateTotalPrice();
}
@Override
public void assertOrderSummary(double totalPrice) {
validateOrderStrategy();
orderPurchaseStrategy.assertOrderSummary(totalPrice);
}
private void validateOrderStrategy() {
if (orderPurchaseStrategy == null) {
throw new NullPointerException(
"The OrderPurchaseStrategy should be first initialized."
);
}
}
}
This abstract class holds a couple of relevant variables. The most prominent one is orderPurchaseStrategy that is initialized in the constructor. It contains a reference to the object that is currently extended. The other variables are used for the computations of the different expected amounts.
Suppose we want to add logic to the above strategy, such as applying VAT Tax and its assertion. We can use the VatTaxOrderPurchaseStrategy, which in its essence is a decorator that is capable of extending other purchase strategies.
public class VatTaxOrderPurchaseStrategy
extends OrderPurchaseStrategyDecorator {
private final VatTaxCalculationService vatTaxCalculationService;
private final CouponCodeCalculationService couponCodeCalculationService;
private double vatTax;
public VatTaxOrderPurchaseStrategy(
OrderPurchaseStrategy orderPurchaseStrategy,
double itemPrice,
PurchaseInfo purchaseInfo
) {
super(orderPurchaseStrategy, itemPrice, purchaseInfo);
vatTaxCalculationService = new VatTaxCalculationService();
couponCodeCalculationService = new CouponCodeCalculationService();
}
@Override
public double calculateTotalPrice() {
var currentCountry = Arrays
.stream(Country.values())
.filter(country -> country.toString().equals(purchaseInfo.getCountry()))
.toArray(Country[]::new)[0];
vatTax =
vatTaxCalculationService.calculate(
(
itemPrice -
couponCodeCalculationService.calculate(
itemPrice,
purchaseInfo.getCouponCode()
)
),
currentCountry
);
return orderPurchaseStrategy.calculateTotalPrice() + vatTax;
}
}
The VatTaxOrderPurchaseStrategy is a descendant of the OrderPurchaseStrategyDecorator. Further, it overrides its methods. The interesting part is that the total price is calculated through a method recursion. First, the total amount is determined by the concrete component (order purchase strategy), and then the computed VAT tax is added to it.
The same recursion technique is used for the validation of the order summary UI. Before anything else, the ValidateOrderSummary methods of all extended strategies are going to be executed, and after that, the VAT tax is verified.
The coupon code discount can be checked through a similar decorator.
public class VatTaxOrderPurchaseStrategy
extends OrderPurchaseStrategyDecorator {
private final VatTaxCalculationService vatTaxCalculationService;
private final CouponCodeCalculationService couponCodeCalculationService;
private double vatTax;
public VatTaxOrderPurchaseStrategy(
OrderPurchaseStrategy orderPurchaseStrategy,
double itemPrice,
PurchaseInfo purchaseInfo
) {
super(orderPurchaseStrategy, itemPrice, purchaseInfo);
vatTaxCalculationService = new VatTaxCalculationService();
couponCodeCalculationService = new CouponCodeCalculationService();
}
@Override
public double calculateTotalPrice() {
var currentCountry = Arrays
.stream(Country.values())
.filter(country -> country.toString().equals(purchaseInfo.getCountry()))
.toArray(Country[]::new)[0];
vatTax =
vatTaxCalculationService.calculate(
(
itemPrice -
couponCodeCalculationService.calculate(
itemPrice,
purchaseInfo.getCouponCode()
)
),
currentCountry
);
return orderPurchaseStrategy.calculateTotalPrice() + vatTax;
}
}
The only difference between the latter and the former is how the total price is calculated.
Usage of Decorated Strategies PurchaseContext
public class PurchaseContext {
private final OrderPurchaseStrategy orderPurchaseStrategy;
private final ItemPage itemPage;
private final ShoppingCartPage shoppingCartPage;
private final CheckoutPage checkoutPage;
public PurchaseContext(OrderPurchaseStrategy orderPurchaseStrategy) {
this.orderPurchaseStrategy = orderPurchaseStrategy;
itemPage = new ItemPage();
shoppingCartPage = new ShoppingCartPage();
checkoutPage = new CheckoutPage();
}
public void purchaseItem(String itemUrl, PurchaseInfo clientPurchaseInfo) {
itemPage.navigate(itemUrl);
itemPage.clickBuyNowButton();
itemPage.clickViewShoppingCartButton();
shoppingCartPage.clickProceedToCheckoutButton();
checkoutPage.fillBillingInfo(clientPurchaseInfo);
var expectedTotalPrice = orderPurchaseStrategy.calculateTotalPrice();
orderPurchaseStrategy.assertOrderSummary(expectedTotalPrice);
}
}
The following code is now missing in the improved version.
public void validatePurchaseInfo(PurchaseInfo purchaseInfo) {
for (var currentStrategy : orderPurchaseStrategies) {
currentStrategy.validatePurchaseInfo(purchaseInfo);
}
}
public void validateOrderSummary(double itemPrice, PurchaseInfo purchaseInfo) {
for (var currentStrategy : orderPurchaseStrategies) {
currentStrategy.assertOrderSummary(itemPrice, purchaseInfo);
}
}
The PurchaseContext holds only one reference to the OrderPurchaseStrategy and employs it to verify the total amount and all other prices on the order summary page.
Decorator Design Pattern Usages in Tests
public class StorePurchaseDecoratedStrategiesTests {
@BeforeMethod
public void testInit() {
Driver.startBrowser();
}
@AfterMethod
public void testCleanup() {
Driver.stopBrowser();
}
@Test
public void totalPriceCalculatedCorrect_when_AtCheckoutAndDecoratedStrategyPatternUsed() {
var itemUrl = "falcon-9";
var itemPrice = 50.00;
var purchaseInfo = new PurchaseInfo();
purchaseInfo.setEmail("info@berlinspaceflowers.com");
purchaseInfo.setFirstName("Anton");
purchaseInfo.setLastName("Angelov");
purchaseInfo.setCompany("Space Flowers");
purchaseInfo.setCountry("Germany");
purchaseInfo.setAddress1("1 Willi Brandt Avenue Tiergarten");
purchaseInfo.setAddress2("Lützowplatz 17");
purchaseInfo.setCity("Berlin");
purchaseInfo.setZip("10115");
purchaseInfo.setPhone("+491888999281");
purchaseInfo.setCouponCode("happybirthday");
OrderPurchaseStrategy orderPurchaseStrategy = new TotalPriceOrderPurchaseStrategy(
itemPrice
);
orderPurchaseStrategy =
new VatTaxOrderPurchaseStrategy(
orderPurchaseStrategy,
itemPrice,
purchaseInfo
);
orderPurchaseStrategy =
new CouponCodeOrderPurchaseStrategy(
orderPurchaseStrategy,
itemPrice,
purchaseInfo
);
new PurchaseContext(orderPurchaseStrategy)
.purchaseItem(itemUrl, purchaseInfo);
}
}
The most prominent part of the above code is how the order purchase strategies are decorated and utilized by the PurchaseContext.
OrderPurchaseStrategy orderPurchaseStrategy = new TotalPriceOrderPurchaseStrategy(itemPrice);
orderPurchaseStrategy = new VatTaxOrderPurchaseStrategy(orderPurchaseStrategy, itemPrice, purchaseInfo);
orderPurchaseStrategy = new CouponCodeOrderPurchaseStrategy(orderPurchaseStrategy, itemPrice, purchaseInfo);
new PurchaseContext(orderPurchaseStrategy).purchaseItem(itemUrl, purchaseInfo);
First, a TotalPriceOrderPurchaseStrategy is instantiated. Then it is passed to the constructor of the VatTaxOrderPurchaseStrategy. This way, it is extended, and the VAT tax is going to be added to the total price. The same is done for coupon code strategy; a new decorator is initialized. Finally, the total price is going to be equal to the item price plus the VAT tax minus the coupon code discount.
Summary – Pros and Cons of using Decorator Design Pattern
Pros and Cons
- Provide a flexible alternative to subclassing for extending functionality.
- Allow behavior modification at runtime rather than going back into existing code and making changes.
- Help resolve the Class Explosion Problem.
- Support the Open-Closed Principle.
- Decorators can result in many small objects, and overuse can be complicated.
- It can complicate the process of instantiating the component because you have to instantiate the component and wrap it in some decorators.
- It can be complicated to have decorators keep track of other decorators because to look back into multiple layers of the decorator chain starts to push the decorator pattern beyond its actual intent.
- Can cause issues if the client relies heavily on the components concrete type.
For 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, Java Edition, Clean Code for Bulletproof Tests”. (+ with the book you will get an access to more than 20000+ lines of real-world code examples and video explanations to solidify your knowledge)
