Introduction
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.
- Reusability
- Maintainability
- Readability
- Easy Testing
- Loose coupling of business rules from the business objects
UML Class Diagram
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; }
}
On the last step of the purchase wizard, there is a page that we are going to call PlaceOrderPage
. There are a couple of possible scenarios that can be performed on this page. Complete the purchase via credit card or wire transfer. Enter a promotional code that applies a discount. Complete the order as a free purchase. Usually, the whole order process contains a lot of steps. This means that there are a lot of combinations between the different steps. Sometimes, some of them won’t be executed, e.g. the client won’t always activate a promo code. So there won’t be the need to assert the promo code label. However, in general, the core workflow stays the same.
The most obvious solution to this problem will be to build the workflow of methods for every possible test case. Nonetheless, if there is a slight change in the workflow, this will force us to change this order in all tests and possibly introduce regression.
I think a better approach to the problem is to build the workflow in one place, e.g., Facade (read more about facades in my post - <a href="http://automatetheplanet.com/improved-facade-design-pattern/">Improved Facade Design Pattern in Automation Testing v.2.0</a>
). Some of the methods might not be executed based on the PurchaseTestInput
.
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");
}
}
}
When you call the ChoosePaymentMethod
method, runtime it will be decided based on the input object how the order will be completed- via credit card or wire transfer.
However, as you can guess, these rules cannot be reused this way. Also, the <a href="http://www.oodesign.com/single-responsibility-principle.html">Single Responsibility Principle</a>
is not followed. Similar conditional logic may be needed in various other scenarios like a different type of asserts.
In my opinion, Specification Design Pattern is the perfect fix for this kind of situations.
As you can see from the participants list, the first thing that we need is the ISpecification<TEntity>
interface.
public interface ISpecification<TEntity>
{
bool IsSatisfiedBy(TEntity entity);
ISpecification<TEntity> And(ISpecification<TEntity> other);
ISpecification<TEntity> Or(ISpecification<TEntity> other);
ISpecification<TEntity> Not();
}
It defines the methods that will be used to chain the different business rules and the primary method IsSatisfiedBy
.
The implementation of the And
, Or
, Not
methods is the same across all the specifications, only the IsSatisfiedBy
varies based on the business rule. So, we define the abstract
class called Specification
that implements the And
, Or
, Not
methods and leave the IsSatisfiedBy
method to its child classes to implement by declaring this method as abstract.
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);
}
}
The only job of this class is to implement the IsSatisfiedBy
method and chain the specifications with And
boolean operator.
OrSpecification
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);
}
}
The accepted specifications are chained with Or
boolean operator in the IsSatisfiedBy
method.
NotSpecification
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);
}
}
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);
}
}
The class inherits the Specification
abstract class and implements the IsSatisfiedBy
method where the credit card related condition is moved.
Here is one more example regarding the promotional purchases’ conditions.
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 @"http://www.bing.com/";
}
}
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");
}
}
}
If you need to change how it is determined if the test purchase should be completed via credit card or wire transfer, you can do it only in a single place- specification classes.
The only problem here is that if you need to do similar conditional logic in your static assert classes, you don’t have access to the specifications. I don’t think it is a good idea to expose the specifications through the page itself because of that we can expose special boolean properties.
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 @"http://www.bing.com/";
}
}
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 information 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.
Summary
The Specification Design Pattern can be used to improve the reusability
, maintainability
, and readability
of your tests. Also, it gives you loose coupling of the business rules from the business objects. If needed, the specifications are easy for testing.
In the next article from the series, I am going to show you how to improve the Specification Design Pattern even more. How to use LINQ to configure the concrete specifications without the need for additional classes, create extension methods for the abstract
base class and how to decouple the specifications from the pages through the usage of test context objects.
So Far in the "Design Patterns in Automated Testing" Series
- Page Object Pattern
- Advanced Page Object Pattern
- Facade Design Pattern
- Singleton Design Pattern
- Fluent Page Object Pattern
- IoC Container and Page Objects
- Strategy Design Pattern
- Advanced Strategy Design Pattern
- Observer Design Pattern
- Observer Design Pattern via Events and Delegates
- Observer Design Pattern via IObservable and IObserver
- Decorator Design Pattern- Mixing Strategies
- Page Objects That Make Code More Maintainable
- Improved Facade Design Pattern in Automation Testing v.2.0
- Rules Design Pattern
- Specification Design Pattern
- Advanced Specification Design Pattern
If you enjoy my publications, feel free to SUBSCRIBE
Also, hit these share buttons. Thank you!
Source Code
References
CodeProject
The post- Specification Design Pattern in Automated Testing appeared first on Automate The Planet.
All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement