Specification Design Pattern in Automated Testing

Specification Design Pattern in Automated Testing

If you follow my series about Design Patterns in Automated Testing, I explain how you can utilize the power of various design patterns in your tests. In the current publication, I am going to share with you the idea how your automation can benefit from the usage of Specification Design Pattern. It is a little bit different from the previously presented Rules Design Pattern. Its main idea is to separate individual rules from the rules processing logic.

Definition

In computer programming, the specification pattern is a particular software design pattern, whereby business rules can be recombined by chaining the business rules together using boolean logic.

Benefits

  • Reusability
  • Maintainability
  • Readability
  • Easy Testing
  • Loose coupling of business rules from the business objects

UML Class Diagram

classDiagram
    ISpecification <|.. Specification
    Specification <|-- AndSpecification
    Specification <|-- OrSpecification
    Specification <|-- NotSpecification
    class ISpecification {
        <<interface>>
        +bool IsSatisfiedBy(object entity)
        +ISpecification And(ISpecification other)
        +ISpecification Not()
        +ISpecification Or(ISpecification other)
    }
    class Specification {
        +bool IsSatisfiedBy(object entity)
        +ISpecification And(ISpecification other)
        +ISpecification Not()
        +ISpecification Or(ISpecification other)
    }
    class AndSpecification {
        +ISpecification leftSpecification
        +ISpecification rightSpecification
        +bool IsSatisfiedBy(object entity)
    }
    class OrSpecification {
        +ISpecification leftSpecification
        +ISpecification rightSpecification
        +bool IsSatisfiedBy(object entity)
    }
    class NotSpecification {
        +ISpecification specification
        +bool IsSatisfiedBy(object entity)
    }

Participants

The classes and objects participating in Specification Design Pattern are:

  • ISpecification

    Defines the interface for all specifications.

  • Specification

    An abstract class that contains the implementation of the And, Or and Not methods. Only the IsSatisfiedBy varies based on the business rule.

  • AndSpecification

    Specification class used for chaining purposes defines the “And” boolean operator.

  • OrSpecification

    Defines the “Or” boolean operator.

  • NotSpecification

    Defines the “Not” boolean operator.

  • CreditCardSpecification

    A concrete specification where the IsSatisfiedBy method is implemented. Holds the concrete business rule.

Specification Design Pattern C# Code

Test’s Test Case

Consider that we have to automate a shopping cart process. During the purchase, we can create orders via wire transfer, credit card or free ones through promotions. Our tests’ workflow is based on a purchase input object that holds all data related to the current purchase e.g. type of purchase and the total price.

public class PurchaseTestInput
{
    public bool IsWiretransfer { get; set; }

    public bool IsPromotionalPurchase { get; set; }

    public string CreditCardNumber { get; set; }

    public decimal TotalPrice { get; set; }
}
public partial class PlaceOrderPage : BasePage
{
    private readonly PurchaseTestInput purchaseTestInput;

    public PlaceOrderPage(IWebDriver driver, PurchaseTestInput purchaseTestInput) : base(driver)
    {
        this.purchaseTestInput = purchaseTestInput;
    }

    public override string Url
    {
        get
        {
            return @"http://www.bing.com/";
        }
    }

    public void ChoosePaymentMethod()
    {
        if (!string.IsNullOrEmpty(this.purchaseTestInput.CreditCardNumber)
            && !this.purchaseTestInput.IsWiretransfer
            && !(this.purchaseTestInput.IsPromotionalPurchase && this.purchaseTestInput.TotalPrice < 5)
            && !(this.purchaseTestInput.TotalPrice == 0))
        {
            this.CreditCard.SendKeys("371449635398431");
            this.SecurityNumber.SendKeys("1234");
        }
        else
        {
            this.Wiretransfer.SendKeys("pathToFile");
        }
    }
}
public interface ISpecification<TEntity>
{
    bool IsSatisfiedBy(TEntity entity);

    ISpecification<TEntity> And(ISpecification<TEntity> other);

    ISpecification<TEntity> Or(ISpecification<TEntity> other);

    ISpecification<TEntity> Not();
}
public abstract class Specification<TEntity> : ISpecification<TEntity>
{
    public abstract bool IsSatisfiedBy(TEntity entity);

    public ISpecification<TEntity> And(ISpecification<TEntity> other)
    {
        return new AndSpecification<TEntity>(this, other);
    }

    public ISpecification<TEntity> Or(ISpecification<TEntity> other)
    {
        return new OrSpecification<TEntity>(this, other);
    }

    public ISpecification<TEntity> Not()
    {
        return new NotSpecification<TEntity>(this);
    }
}

The And, Or and Not methods create and return an AndSpecification, OrSpecification, and NotSpecification object respectively. These classes are used mainly for chaining purposes. The AndSpecification and OrSpecification classes accept two ISpecification parameters, unlike NotSpecification which is needs just one, considering the fact that former ones are binary operators and the later being unary.

AndSpecification

public class AndSpecification<TEntity> : Specification<TEntity>
{
    private readonly ISpecification<TEntity> leftSpecification;
    private readonly ISpecification<TEntity> rightSpecification;

    public AndSpecification(ISpecification<TEntity> leftSpecification, ISpecification<TEntity> rightSpecification)
    {
        this.leftSpecification = leftSpecification;
        this.rightSpecification = rightSpecification;
    }

    public override bool IsSatisfiedBy(TEntity entity)
    {
        return this.leftSpecification.IsSatisfiedBy(entity) && this.rightSpecification.IsSatisfiedBy(entity);
    }
}
public class OrSpecification<TEntity> : Specification<TEntity>
{
    private readonly ISpecification<TEntity> leftSpecification;
    private readonly ISpecification<TEntity> rightSpecification;

    public OrSpecification(ISpecification<TEntity> leftSpecification, ISpecification<TEntity> rightSpecification)
    {
        this.leftSpecification = leftSpecification;
        this.rightSpecification = rightSpecification;
    }

    public override bool IsSatisfiedBy(TEntity entity)
    {
        return this.leftSpecification.IsSatisfiedBy(entity) || this.rightSpecification.IsSatisfiedBy(entity);
    }
}
public class NotSpecification<TEntity> : Specification<TEntity>
{
    private readonly ISpecification<TEntity> specification;

    public NotSpecification(ISpecification<TEntity> specification)
    {
        this.specification = specification;
    }

    public override bool IsSatisfiedBy(TEntity entity)
    {
        return !this.specification.IsSatisfiedBy(entity);
    }
}

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

Refactor PlaceOrderPage to Use Specification Design Pattern

We can move the different purchase-related conditions in a couple of specification classes.

public class CreditCardSpecification : Specification<PurchaseTestInput>
{
    private readonly PurchaseTestInput purchaseTestInput;

    public CreditCardSpecification(PurchaseTestInput purchaseTestInput)
    {
        this.purchaseTestInput = purchaseTestInput;
    }

    public override bool IsSatisfiedBy(PurchaseTestInput entity)
    {
        return !string.IsNullOrEmpty(this.purchaseTestInput.CreditCardNumber);
    }
}
public class PromotionalPurchaseSpecification : Specification<PurchaseTestInput>
{
    private readonly PurchaseTestInput purchaseTestInput;

    public PromotionalPurchaseSpecification(PurchaseTestInput purchaseTestInput)
    {
        this.purchaseTestInput = purchaseTestInput;
    }

    public override bool IsSatisfiedBy(PurchaseTestInput entity)
    {
        return this.purchaseTestInput.IsPromotionalPurchase && this.purchaseTestInput.TotalPrice < 5;
    }
}

Now if we use the newly created specification, we can refactor the PlaceOrderPage class. It will look like the code below.

public partial class PlaceOrderPage : BasePage
{
    private readonly PurchaseTestInput purchaseTestInput;
    private readonly PromotionalPurchaseSpecification promotionalPurchaseSpecification;
    private readonly CreditCardSpecification creditCardSpecification;
    private readonly WiretransferSpecification wiretransferSpecification;
    private readonly FreePurchaseSpecification freePurchaseSpecification;

    public PlaceOrderPage(IWebDriver driver, PurchaseTestInput purchaseTestInput) : base(driver)
    {
        this.purchaseTestInput = purchaseTestInput;
        this.promotionalPurchaseSpecification = new PromotionalPurchaseSpecification(purchaseTestInput);
        this.wiretransferSpecification = new WiretransferSpecification(purchaseTestInput);
        this.creditCardSpecification = new CreditCardSpecification(purchaseTestInput);
        this.freePurchaseSpecification = new FreePurchaseSpecification();
    }

    public override string Url
    {
        get
        {
            return @"yourSiteUrl";
        }
    }

    public void ChoosePaymentMethod()
    {
        if (this.creditCardSpecification.
        And(this.wiretransferSpecification.Not()).
        And(this.freePurchaseSpecification.Not()).
        And(this.promotionalPurchaseSpecification.Not()).
        IsSatisfiedBy(this.purchaseTestInput))
        {
            this.CreditCard.SendKeys("371449635398431");
            this.SecurityNumber.SendKeys("1234");
        }
        else
        {
            this.Wiretransfer.SendKeys("pathToFile");
        }
    }
}
public partial class PlaceOrderPage : BasePage
{
    private readonly PurchaseTestInput purchaseTestInput;
    private readonly PromotionalPurchaseSpecification promotionalPurchaseSpecification;
    private readonly CreditCardSpecification creditCardSpecification;
    private readonly WiretransferSpecification wiretransferSpecification;
    private readonly FreePurchaseSpecification freePurchaseSpecification;

    public PlaceOrderPage(IWebDriver driver, PurchaseTestInput purchaseTestInput) : base(driver)
    {
        this.purchaseTestInput = purchaseTestInput;
        this.promotionalPurchaseSpecification = new PromotionalPurchaseSpecification(purchaseTestInput);
        this.wiretransferSpecification = new WiretransferSpecification(purchaseTestInput);
        this.creditCardSpecification = new CreditCardSpecification(purchaseTestInput);
        this.freePurchaseSpecification = new FreePurchaseSpecification();
        this.IsPromoCodePurchase = this.freePurchaseSpecification.Or(this.promotionalPurchaseSpecification).IsSatisfiedBy(this.purchaseTestInput);
        this.IsCreditCardPurchase = this.creditCardSpecification.
        And(this.wiretransferSpecification.Not()).
        And(this.freePurchaseSpecification.Not()).
        And(this.promotionalPurchaseSpecification.Not()).
        IsSatisfiedBy(this.purchaseTestInput);
    }

    public bool IsPromoCodePurchase { get; private set; }

    public bool IsCreditCardPurchase { get; private set; }

    public override string Url
    {
        get
        {
            return @"pathToYourSite";
        }
    }

    public void ChoosePaymentMethod()
    {
        if (this.IsCreditCardPurchase)
        {
            this.CreditCard.SendKeys("371449635398431");
            this.SecurityNumber.SendKeys("1234");
        }
        else
        {
            this.Wiretransfer.SendKeys("pathToFile");
        }
    }

    public void TypePromotionalCode(string promoCode)
    {
        if (this.IsPromoCodePurchase)
        {
            this.PromotionalCode.SendKeys(promoCode);
        }
    }
}

The IsCreditCardPurchase holds the info if the purchase should be completed via credit card. The IsPromoCodePurchase has a similar purpose. Both can be used in the conditions on the page itself, as well as in the static assert extension methods of the page.

public static class PlaceOrderPageAsserter
{
    public static void AssertPromoCodeLabel(this PlaceOrderPage page, string promoCode)
    {
        if (!string.IsNullOrEmpty(promoCode) && page.IsPromoCodePurchase)
        {
            Assert.AreEqual<string>(page.PromotionalCode.Text, promoCode);
        }
    }
}

The extension method accepts the page as a parameter, so it has access to previously created boolean properties.

Related Articles

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#

Design Patterns

Composite Design Pattern in Automated Testing

Achieving high-quality test automation that brings value- you need to understand core programming concepts such as SOLID and the usage of design patterns. In th

Composite Design Pattern in Automated Testing

Design Patterns

Advanced Null Object Design Pattern in Automated Testing

This is the second article dedicated to the Null Object Design Pattern part of the Design Patterns in Automated Testing series. In the last post, I showed you h

Advanced Null Object Design Pattern in Automated Testing

Design Patterns

Page Objects with Partial Classes and Properties- WebDriver C#

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

Page Objects with Partial Classes and Properties- WebDriver C#

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 Architecture, Design Patterns

Failed Tests Аnalysis – Decorator Design Pattern

Here I will present to you the third version of the Failed Tests Analysis engine part of the Design Patterns in Automated Testing Series. We are going to utilis

Failed Tests Аnalysis – Decorator Design Pattern
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.