Decorator Design Pattern in Automated Testing

Decorator Design Pattern in Automated Testing

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.

Note

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

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

classDiagram
    Component <|-- ConcreteComponent
    Component <|-- Decorator
    Decorator o-- Component
    Decorator <|-- ConcreteDecorator
    class Component {
        +Operation()
    }
    class ConcreteComponent {
        +Operation()
    }
    class Decorator {
        -Component component
        +Operation()
    }
    class ConcreteDecorator {
        +Operation()
        +AddedBehavior()
    }

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.

Test’s Test Case

Amazon Items Page

Login Existing Client Amazon

4. Fill Shipping Info

Fill Shipping Info Amazon

Select Payment Method Amazon

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);
    }
}

While ago when we were working on the first version of the BELLATRIX test automation framework, I did this research and 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 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);
classDiagram
    OrderPurchaseStrategy <|-- GiftOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- VatTaxOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- SalesTaxOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- NoTaxesOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- VatTaxGiftOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- SalesTaxGiftOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- VatSalesTaxOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- VatSalesTaxGiftOrderPurchaseStrategy
    class OrderPurchaseStrategy {
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class GiftOrderPurchaseStrategy {
        +giftWrappingPriceCalculationService
        +giftWrapPrice
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class VatTaxOrderPurchaseStrategy {
        +vatTax
        +vatTaxCalculationService
        +CalculateTotalPrice()
    }
    class SalesTaxOrderPurchaseStrategy {
        +salesTax
        +salesTaxCalculationService
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class NoTaxesOrderPurchaseStrategy {
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class VatTaxGiftOrderPurchaseStrategy {
        +giftWrappingPriceCalculationService
        +giftWrapPrice
        +vatTax
        +vatTaxCalculationService
        +CalculateTotalPrice()
    }
    class SalesTaxGiftOrderPurchaseStrategy {
        +giftWrappingPriceCalculationService
        +giftWrapPrice
        +salesTax
        +salesTaxCalculationService
    }
    class VatSalesTaxOrderPurchaseStrategy {
        +salesTax
        +salesTaxCalculationService
        +vatTax
        +vatTaxCalculationService
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class VatSalesTaxGiftOrderPurchaseStrategy {
        +giftWrappingPriceCalculationService
        +giftWrapPrice
        +salesTax
        +salesTaxCalculationService
        +vatTax
        +vatTaxCalculationService
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }

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.

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, C# Edition, High-Quality Tests Attributes, and Best Practices”.  You can read part of three of the chapters:

Defining High-Quality Test Attributes for Automated Tests

Benchmarking for Assessing Automated Test Components Performance

Generic Repository Design Pattern- Test Data Preparation

Specific UML Class Diagram

classDiagram
    OrderPurchaseStrategy <|-- TotalPriceOrderPurchaseStrategy
    OrderPurchaseStrategy <|-- OrderPurchaseStrategyDecorator
    OrderPurchaseStrategyDecorator o-- OrderPurchaseStrategy
    OrderPurchaseStrategyDecorator <|-- VatTaxOrderPurchaseStrategy
    class OrderPurchaseStrategy {
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class TotalPriceOrderPurchaseStrategy {
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class OrderPurchaseStrategyDecorator {
        -OrderPurchaseStrategy strategy
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }
    class VatTaxOrderPurchaseStrategy {
        +CalculateTotalPrice()
        +ValidateOrderSummary(decimal totalPrice)
    }

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.

public abstract class OrderPurchaseStrategy
{
    public abstract decimal CalculateTotalPrice();

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

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());
    }
}
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 Usages in Tests


public class Online StorePurchase_DecoratedStrategies_Tests
{
    
public void SetupTest()
{
    Driver.StartBrowser();
}


public void TeardownTest()
{
    Driver.StopBrowser();
}


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

Pros and Cons Decorator Design Pattern

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

Related Articles

Design Patterns, Web Automation Java

Mastering Parameterized Tests in JUnit with Selenium WebDriver

In the evolving landscape of software testing, efficiency and coverage are paramount. JUnit 5 introduces enhanced parameterized testing capabilities, allowing d

Mastering Parameterized Tests in JUnit with Selenium WebDriver

Design Patterns

Enhanced Selenium WebDriver Page Objects through Partial Classes

Editorial Note: I originally wrote this post for the Test Huddle Blog. You can check out the original text at their site.

Enhanced Selenium WebDriver Page Objects through Partial Classes

Design Patterns

Simple Factory Design Pattern- WebDriver Anonymous Browsing with Reverse Proxy

In the series “Design Patterns in Automated Testing“, you can read about the most useful techniques for structuring the automation tests' code. The article was

Simple Factory Design Pattern- WebDriver Anonymous Browsing with Reverse Proxy

Design Patterns

Advanced Behaviours Design Pattern in Automated Testing Part 2

My last two articles were dedicated to the Behaviours Design Pattern. It is a pattern that eases the creation of tests through a build process similar to LEGO.

Advanced Behaviours Design Pattern in Automated Testing Part 2

Design Patterns

Fluent Page Object Pattern in Automated Testing

In my previous articles from the series "Design Patterns in Automated Testing", I explained in details how to improve your test automation framework through the

Fluent Page Object Pattern in Automated Testing

Design Patterns

Page Objects- App Design Pattern- WebDriver C#

Editorial Note: I originally wrote this post for the Test Huddle Blog. You can check out the original here, at their site.

Page Objects- App Design Pattern- WebDriver C#
Anton Angelov

About the author

Anton Angelov is Managing Director, Co-Founder, and Chief Test Automation Architect at Automate The Planet — a boutique consulting firm specializing in AI-augmented test automation strategy, implementation, and enablement. He is the creator of BELLATRIX, a cross-platform framework for web, mobile, desktop, and API testing, and the author of 8 bestselling books on test automation. A speaker at 60+ international conferences and researcher in AI-driven testing and LLM-based automation, he has been recognized as QA of the Decade and Webit Changemaker 2025.