Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Decorator Design Pattern in Automation Testing

DZone's Guide to

Decorator Design Pattern in Automation Testing

The benefits of the decorator design pattern in your automation tests, and how to use it in a sample test to buy items off of Amazon.

· DevOps Zone
Free Resource

Download the blueprint that can take a company of any maturity level all the way up to enterprise-scale continuous delivery using a combination of Automic Release Automation, Automic’s 20+ years of business automation experience, and the proven tools and practices the company is already leveraging.

In my articles “Strategy Design Pattern” and “Advanced Strategy Design Pattern," I explained the benefits of the application of Strategy Design Pattern 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 Principlethat 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 Decorator Design Pattern. In this publication, I’m going to 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 allow 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.

Definition

The Decorator Design Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

  • You can wrap a component with any number of decorators.
  • Change the behavior of its component by adding new functionality before and/or after method calls to the component.
  • Decorator classes mirror the type of the components they decorate.
  • Provides an alternative to subclassing for extending behavior.

Abstract UML Class Diagram

Abstract UML Diagram Decorator Design Pattern

Participants

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 are going to decorate. The decorator has a HAS-A relationship with the object that is extending, which means that the former has an instance variable that holds a reference to the later.
  • ConcreteComponent – 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 C# Code

Test’s Test Case

The test case of the examples is going to be the same as of the previous articles. The primary goal is going to be to purchase different items from Amazon. Also, the prices on the last step of the buying process should be validated- taxes, shipping costs, gift wrapping expenses, etc.

1. Navigate to Item’s Page

Amazon Items Page

 

2. Click Proceed to Checkout

3. Login with an existing Client

Login Existing Client Amazon

 

4. Fill Shipping Info

Fill Shipping Info Amazon

 

























5. Choose a shipping speed

6. Select a payment method

Select Payment Method Amazon

 

7. Validate the Order Summary

Validate Order Summary Amazon

 

The previous articles explain in details how to automate the whole purchase process. However, to introduce the benefits of the Decorator Design Pattern, only the last step is going to be necessary- Order Summary Validation. In the posts about the Strategy Design Pattern, the prices on the last step of the purchase process are validated through the help of different Validation Strategies that implement the IOrderPurchaseStrategy.


public class PurchaseContext
{
private readonly IOrderValidationStrategy orderValidationStrategy;
public PurchaseContext(IOrderValidationStrategy orderValidationStrategy)
{
this.orderValidationStrategy = orderValidationStrategy;
}
public void PurchaseItem(string itemUrl, string itemPrice, ClientLoginInfo clientLoginInfo, ClientPurchaseInfo clientPurchaseInfo)
{
ItemPage.Instance.Navigate(itemUrl);
ItemPage.Instance.ClickBuyNowButton();
PreviewShoppingCartPage.Instance.ClickProceedToCheckoutButton();
SignInPage.Instance.Login(clientLoginInfo.Email, clientLoginInfo.Password);
ShippingAddressPage.Instance.FillShippingInfo(clientPurchaseInfo);
ShippingAddressPage.Instance.ClickContinueButton();
ShippingPaymentPage.Instance.ClickBottomContinueButton();
ShippingPaymentPage.Instance.ClickTopContinueButton();
this.orderValidationStrategy.ValidateOrderSummary(itemPrice, clientPurchaseInfo);
}
}


Improved Version Advanced Strategy Design Pattern Applied

public class PurchaseContext
{
private readonly IOrderPurchaseStrategy[] orderpurchaseStrategies;
public PurchaseContext(params IOrderPurchaseStrategy[] orderpurchaseStrategies)
{
this.orderpurchaseStrategies = orderpurchaseStrategies;
}
public void PurchaseItem(string itemUrl, string itemPrice, ClientLoginInfo clientLoginInfo, ClientPurchaseInfo clientPurchaseInfo)
{
this.ValidateClientPurchaseInfo(clientPurchaseInfo);
ItemPage.Instance.Navigate(itemUrl);
ItemPage.Instance.ClickBuyNowButton();
PreviewShoppingCartPage.Instance.ClickProceedToCheckoutButton();
SignInPage.Instance.Login(clientLoginInfo.Email, clientLoginInfo.Password);
ShippingAddressPage.Instance.FillShippingInfo(clientPurchaseInfo);
ShippingAddressPage.Instance.ClickDifferentBillingCheckBox(clientPurchaseInfo);
ShippingAddressPage.Instance.ClickContinueButton();
ShippingPaymentPage.Instance.ClickBottomContinueButton();
ShippingAddressPage.Instance.FillBillingInfo(clientPurchaseInfo);
ShippingAddressPage.Instance.ClickContinueButton();
ShippingPaymentPage.Instance.ClickTopContinueButton();
this.ValidateOrderSummary(itemPrice, clientPurchaseInfo);
}
public void ValidateClientPurchaseInfo(ClientPurchaseInfo clientPurchaseInfo)
{
foreach (var currentStrategy in orderpurchaseStrategies)
{
currentStrategy.ValidateClientPurchaseInfo(clientPurchaseInfo);
}
}
public void ValidateOrderSummary(string itemPrice, ClientPurchaseInfo clientPurchaseInfo)
{
foreach (var currentStrategy in orderpurchaseStrategies)
{
currentStrategy.ValidateOrderSummary(itemPrice, clientPurchaseInfo);
}
}
}


The usage of the PurchaseContext is not so straightforward as you can see from the code below.

new PurchaseContext(new SalesTaxOrderPurchaseStrategy(), new VatTaxOrderPurchaseStrategy(), new GiftOrderPurchaseStrategy())
.PurchaseItem(itemUrl, itemPrice, clientLoginInfo, clientPurchaseInfo);


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 IOrderPurchaseStrategy interface, you need to create a new one with a “foreach” statement in the PurchaseContext class. Also, personally 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- “Strategy Design Pattern” and “Advanced 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.,  VatSalesTaxOrderPurchaseStrategy, SalesTaxGiftOrderPurchaseStrategy, GiftOrderPurchaseStrategy,

NoTaxesOrderPurchaseStrategy, etc. But as you can see this escalated quickly- a typical example of a class explosion.

Decorator Design Pattern Class Explosion

 

If you need to add additional validators, 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

Concrete UML Class Diagram Decorator Design Pattern

Participants

The classes and objects participating in this pattern are:

  • OrderPurchaseStrategy (Component) – Defines the interface for all concrete strategies that are going to validate 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, 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 theOrderPurchaseStrategy, 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 VAT Tax of the order and also adds the new tax to the total price.

Refactor 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 decimal CalculateTotalPrice();
public abstract void ValidateOrderSummary(decimal totalPrice);
}


It holds only two abstract methods.

CalculateTotalPrice – Returns the total price of the order. It depends on the applied taxes and discounts because of that every strategy should implement it.

ValidateOrderSummary – Validates all prices on the order summary page- total price, taxes, discounts, etc.

The first concrete component in the example is the TotalPriceOrderPurchaseStrategy that verifies the correctness of the total price.

public class TotalPriceOrderPurchaseStrategy : OrderPurchaseStrategy
{
private readonly decimal itemsPrice;
public TotalPriceOrderPurchaseStrategy(decimal itemsPrice)
{
this.itemsPrice = itemsPrice;
}
public override decimal CalculateTotalPrice()
{
return itemsPrice;
}
public override void ValidateOrderSummary(decimal totalPrice)
{
PlaceOrderPage.Instance.Validate().OrderTotalPrice(totalPrice.ToString());
}
}


To be able to add a new behavior at runtime dynamically, all decorators need to derive from the class OrderPurchaseStrategyDecorator.

public abstract class OrderPurchaseStrategyDecorator : OrderPurchaseStrategy
{
protected readonly OrderPurchaseStrategy orderPurchaseStrategy;
protected readonly ClientPurchaseInfo clientPurchaseInfo;
protected readonly decimal itemsPrice;
public OrderPurchaseStrategyDecorator(OrderPurchaseStrategy orderPurchaseStrategy, decimal itemsPrice, ClientPurchaseInfo clientPurchaseInfo)
{
this.orderPurchaseStrategy = orderPurchaseStrategy;
this.itemsPrice = itemsPrice;
this.clientPurchaseInfo = clientPurchaseInfo;
}
public override decimal CalculateTotalPrice()
{
this.ValidateOrderStrategy();
return this.orderPurchaseStrategy.CalculateTotalPrice();
}
public override void ValidateOrderSummary(decimal totalPrice)
{
this.ValidateOrderStrategy();
this.orderPurchaseStrategy.ValidateOrderSummary(totalPrice);
}
private void ValidateOrderStrategy()
{
if (this.orderPurchaseStrategy == null)
{
throw new Exception("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.

Decorate Strategies

 














If we want to add logic to the above strategy, for example- application of VAT Tax and its verification. We can use the VatTaxOrderPurchaseStrategy, which in its essence is a decorator that is capable of extending other purchase strategies.

public class VatTaxOrderPurchaseStrategy : OrderPurchaseStrategyDecorator
{
private readonly VatTaxCalculationService vatTaxCalculationService;
private decimal vatTax;
public VatTaxOrderPurchaseStrategy(OrderPurchaseStrategy orderPurchaseStrategy, decimal itemsPrice, ClientPurchaseInfo clientPurchaseInfo)
: base(orderPurchaseStrategy, itemsPrice, clientPurchaseInfo)
{
this.vatTaxCalculationService = new VatTaxCalculationService();
}
public override decimal CalculateTotalPrice()
{
Countries currentCountry = (Countries)Enum.Parse(typeof(Countries), clientPurchaseInfo.BillingInfo.Country);
this.vatTax = this.vatTaxCalculationService.Calculate(this.itemsPrice, currentCountry);
return this.orderPurchaseStrategy.CalculateTotalPrice() + this.vatTax;
}
public override void ValidateOrderSummary(decimal totalPrice)
{
base.orderPurchaseStrategy.ValidateOrderSummary(totalPrice);
PlaceOrderPage.Instance.Validate().EstimatedTaxPrice(vatTax.ToString());
}
}


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 sales tax can be checked through a similar decorator.

public class SalesTaxOrderPurchaseStrategy : OrderPurchaseStrategyDecorator
{
private readonly SalesTaxCalculationService salesTaxCalculationService;
private decimal salesTax;
public SalesTaxOrderPurchaseStrategy(OrderPurchaseStrategy orderPurchaseStrategy, decimal itemsPrice, ClientPurchaseInfo clientPurchaseInfo)
: base(orderPurchaseStrategy, itemsPrice, clientPurchaseInfo)
{
this.salesTaxCalculationService = new SalesTaxCalculationService();
}
public SalesTaxCalculationService SalesTaxCalculationService { get; set; }
public override decimal CalculateTotalPrice()
{
States currentState = (States)Enum.Parse(typeof(States), clientPurchaseInfo.ShippingInfo.State);
this.salesTax = this.salesTaxCalculationService.Calculate(this.itemsPrice, currentState, clientPurchaseInfo.ShippingInfo.Zip);
return this.orderPurchaseStrategy.CalculateTotalPrice() + this.salesTax;
}
public override void ValidateOrderSummary(decimal totalPrice)
{
base.orderPurchaseStrategy.ValidateOrderSummary(totalPrice);
PlaceOrderPage.Instance.Validate().EstimatedTaxPrice(salesTax.ToString());
}
}


The only difference between the latter and the former is how the tax is determined.

Usage of Decorated Strategies PurchaseContext

public class PurchaseContext
{
private readonly OrderPurchaseStrategy orderPurchaseStrategy;
public PurchaseContext(OrderPurchaseStrategy orderPurchaseStrategy)
{
this.orderPurchaseStrategy = orderPurchaseStrategy;
}
public void PurchaseItem(string itemUrl, string itemPrice, ClientLoginInfo clientLoginInfo, ClientPurchaseInfo clientPurchaseInfo)
{
ItemPage.Instance.Navigate(itemUrl);
ItemPage.Instance.ClickBuyNowButton();
PreviewShoppingCartPage.Instance.ClickProceedToCheckoutButton();
SignInPage.Instance.Login(clientLoginInfo.Email, clientLoginInfo.Password);
ShippingAddressPage.Instance.FillShippingInfo(clientPurchaseInfo);
ShippingAddressPage.Instance.ClickDifferentBillingCheckBox(clientPurchaseInfo);
ShippingAddressPage.Instance.ClickContinueButton();
ShippingPaymentPage.Instance.ClickBottomContinueButton();
ShippingAddressPage.Instance.FillBillingInfo(clientPurchaseInfo);
ShippingAddressPage.Instance.ClickContinueButton();
ShippingPaymentPage.Instance.ClickTopContinueButton();
decimal expectedTotalPrice = this.orderPurchaseStrategy.CalculateTotalPrice();
this.orderPurchaseStrategy.ValidateOrderSummary(expectedTotalPrice);
}
}


The following code is now missing in the improved version.

public void ValidateClientPurchaseInfo(ClientPurchaseInfo clientPurchaseInfo)
{
foreach (var currentStrategy in orderpurchaseStrategies)
{
currentStrategy.ValidateClientPurchaseInfo(clientPurchaseInfo);
}
}
public void ValidateOrderSummary(string itemPrice, ClientPurchaseInfo clientPurchaseInfo)
{
foreach (var currentStrategy in orderpurchaseStrategies)
{
currentStrategy.ValidateOrderSummary(itemPrice, clientPurchaseInfo);
}
}


Now 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 Application Tests

 

Decorator Design Pattern Usages in Tests

[TestClass]
public class AmazonPurchase_DecoratedStrategies_Tests
{
[TestInitialize]
public void SetupTest()
{
Driver.StartBrowser();
}
[TestCleanup]
public void TeardownTest()
{
Driver.StopBrowser();
}
[TestMethod]
public void Purchase_SeleniumTestingToolsCookbook_DecoratedStrategies()
{
string itemUrl = "/Selenium-Testing-Cookbook-Gundecha-Unmesh/dp/1849515743";
decimal itemPrice = 40.49m;
var shippingInfo = new ClientAddressInfo()
{
FullName = "John Smith",
Country = "United States",
Address1 = "950 Avenue of the Americas",
State = "Texas",
City = "Houston",
Zip = "77001",
Phone = "00164644885569"
};
var billingInfo = new ClientAddressInfo()
{
FullName = "Anton Angelov",
Country = "Bulgaria",
Address1 = "950 Avenue of the Americas",
City = "Sofia",
Zip = "1672",
Phone = "0894464647"
};
ClientPurchaseInfo clientPurchaseInfo = new ClientPurchaseInfo(billingInfo, shippingInfo)
{
GiftWrapping = GiftWrappingStyles.Fancy
};
ClientLoginInfo clientLoginInfo = new ClientLoginInfo()
{
Email = "g3984159@trbvm.com",
Password = "ASDFG_12345"
};
OrderPurchaseStrategy orderPurchaseStrategy = new TotalPriceOrderPurchaseStrategy(itemPrice);
orderPurchaseStrategy = new SalesTaxOrderPurchaseStrategy(orderPurchaseStrategy, itemPrice, clientPurchaseInfo);
orderPurchaseStrategy = new VatTaxOrderPurchaseStrategy(orderPurchaseStrategy, itemPrice, clientPurchaseInfo);
new PurchaseContext(orderPurchaseStrategy).PurchaseItem(itemUrl, itemPrice.ToString(), clientLoginInfo, clientPurchaseInfo);
}
}


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 SalesTaxOrderPurchaseStrategy(orderPurchaseStrategy, itemPrice, clientPurchaseInfo);
orderPurchaseStrategy = new VatTaxOrderPurchaseStrategy(orderPurchaseStrategy, itemPrice, clientPurchaseInfo);
new PurchaseContext(orderPurchaseStrategy).PurchaseItem(itemUrl, itemPrice.ToString(), clientLoginInfo, clientPurchaseInfo);


First a TotalPriceOrderPurchaseStrategy is instantiated. Then it is passed to the constructor of the SalesTaxOrderPurchaseStrategy, this way it is extended and the sales tax is going to be added to the total price. The same is done for sales tax strategy; a newVatTaxOrderPurchaseStrategy decorator is initialized. Finally, the total price is going to be equal to the item price plus the sales tax plus the VAT tax.

Pros Cons Decorator Design Pattern

 

Pros and Cons Decorator Design Pattern

Cons

  • Decorators can result in many small objects, and overuse can be complicated.

  • Can complicate the process of instantiating the component because you not only have to instantiate the component but 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.

Pros

  • 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.

Source Code

You can download the full source code of the advanced strategy design pattern from my Github Repository.

If you enjoy my publications, feel free to SUBSCRIBE– http://automatetheplanet.com/newsletter/ 

References:

Download the ‘Practical Blueprint to Continuous Delivery’ to learn how Automic Release Automation can help you begin or continue your company’s digital transformation.

Topics:
devops ,automation ,design patterns ,testing

Published at DZone with permission of Anton Angelov, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}