Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Decorator Design Pattern in Automation Testing

5.00/5 (4 votes)
4 Oct 2015Ms-PL8 min read 11K  
A detailed overview with examples how to utilize the Decorator Design Pattern in automated tests to create an extendable and decoupled Validators.

Introduction

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

Image 1

If you are not familiar with the above patterns, I suggest you to read my articles about them first, to be able to understand the presented concepts thoroughly. (Especially the ones related to Strategy Design Pattern).

Definition

Quote:

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

Image 2

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

Image 3

2. Click Proceed to Checkout

3. Login with an existing Client

Image 4

4. Fill Shipping Info

Image 5

5. Choose a shipping speed

6. Select a payment method

Image 6

7. Validate the Order Summary

Image 7

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

C#
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.

C#
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.

Image 8

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

Image 9

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

C#
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.

C#
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.

C#
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.

Image 10

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.

C#
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.

C#
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

C#
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.

C#
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.

Image 11

Decorator Design Pattern Usages in Tests

C#
[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.

C#
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 new VatTaxOrderPurchaseStrategy decorator is initialized. Finally, the total price is going to be equal to the item price plus the sales tax plus the VAT tax.

Image 12

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.

So Far in the "Design Patterns in Automated Testing" Series

  1. Page Object Pattern
  2. Advanced Page Object Pattern
  3. Facade Design Pattern
  4. Singleton Design Pattern
  5. Fluent Page Object Pattern
  6. IoC Container and Page Objects
  7. Strategy Design Pattern
  8. Advanced Strategy Design Pattern
  9. Observer Design Pattern
  10. Observer Design Pattern via Events and Delegates
  11. Observer Design Pattern via IObservable and IObserver
  12. Decorator Design Pattern- Mixing Strategies
  13. Page Objects That Make Code More Maintainable
  14. Improved Facade Design Pattern in Automation Testing v.2.0
  15. Rules Design Pattern
  16. Specification Design Pattern
  17. Advanced Specification Design Pattern

 

If you enjoy my publications, feel free to SUBSCRIBE
Also, hit these share buttons. Thank you!

Source Code

References:

 

All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement

The post- Decorator Design Pattern in Automation Testing appeared first on Automate The Planet.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)