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.
