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

Rules Design Pattern in Automation Testing

5.00/5 (5 votes)
20 Dec 2015Ms-PL6 min read 17.6K  
A detailed overview with examples of how to utilize the Rules Design Pattern in automated tests to separate individual rules from rules processing logic. The post Rules Design Pattern in Automation Testing appeared first on Automate The Planet.

Introduction

As you probably know in my series “Design Patterns in Automation Testing“, I explain the benefits of applying design patterns in your automation projects. In this article, I am going to share with you the aid that you can gain from the usage of Rules Design Pattern. It can help you to reduce the complexity of your conditional statements and reuse them if needed.

Rules Design Pattern

Definition

Separate the logic of each individual rule and its effects into its own class. Separate the selection and processing of rules into a separate Evaluator class.

  • Separate individual rules from rules processing logic.
  • Allow new rules to be added without the need for changes in the rest of the system.

Abstract UML Class Diagram

Rules Design Pattern UML Class Diagram

Participants

The classes and objects participating in this pattern are:

  • IRule – Defines the interface for all specific rules.
  • IRuleResult – Defines the interface for the results of all specific rules.
  • BaseRule – The base class provides basic functionality to all rules that inherit from it.
  • Rule – The class represents a concrete implementation of the BaseRule class.
  • RulesChain – It is a helper class that contains the main rule for the current conditional statement and the rest of the conditional chain of rules.
  • RulesEvaluator – This is the main class that supports the creation of readable rules and their relation. It evaluates the rules and returns their results.

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

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

    public bool IsPromotionalPurchase { get; set; }

    public string CreditCardNumber { get; set; }

    public decimal TotalPrice { get; set; }
}

A sample conditional test workflow for our test logic without any design pattern applied could look like the following code:

C#
PurchaseTestInput purchaseTestInput = new PurchaseTestInput()
{
    IsWiretransfer = false,
    IsPromotionalPurchase = false,
    TotalPrice = 100,
    CreditCardNumber = "378734493671000"
};
if (string.IsNullOrEmpty(purchaseTestInput.CreditCardNumber) &&
    !purchaseTestInput.IsWiretransfer &&
    purchaseTestInput.IsPromotionalPurchase &&
    purchaseTestInput.TotalPrice == 0)
{
    this.PerformUIAssert("Assert volume discount promotion amount. + additional UI actions");
}
if (!string.IsNullOrEmpty(purchaseTestInput.CreditCardNumber) &&
    !purchaseTestInput.IsWiretransfer &&
    !purchaseTestInput.IsPromotionalPurchase &&
    purchaseTestInput.TotalPrice > 20)
{
    this.PerformUIAssert("Assert that total amount label is over 20$ + additional UI actions");
}
else if (!string.IsNullOrEmpty(purchaseTestInput.CreditCardNumber) &&
            !purchaseTestInput.IsWiretransfer &&
            !purchaseTestInput.IsPromotionalPurchase &&
            purchaseTestInput.TotalPrice > 30)
{
    Console.WriteLine("Assert that total amount label is over 30$ + additional UI actions");
}
else if (!string.IsNullOrEmpty(purchaseTestInput.CreditCardNumber) &&
            !purchaseTestInput.IsWiretransfer &&
            !purchaseTestInput.IsPromotionalPurchase &&
            purchaseTestInput.TotalPrice > 40)
{
    Console.WriteLine("Assert that total amount label is over 40$ + additional UI actions");
}
else if (!string.IsNullOrEmpty(purchaseTestInput.CreditCardNumber) &&
    !purchaseTestInput.IsWiretransfer &&
    !purchaseTestInput.IsPromotionalPurchase &&
    purchaseTestInput.TotalPrice > 50)
{
    this.PerformUIAssert("Assert that total amount label is over 50$ + additional UI actions");
}
else
{
    Debug.WriteLine("Perform other UI actions");
}

The actions that can be performed in the conditions may be - applying coupons or other promotions in the UI, completing order via different payment methods in the UI, asserting different things in the UI or the DB. This test workflow is usually wrapped in a method of a facade or similar class.

The main problem with this code is that it is highly unreadable. Also, another thing to consider is that you might need the same rules in different kind of classes- you may want to use the rule once in a UI Facade and a second time in a DB Asserter class.

Improved Version Rules Design Pattern Applied

C#
PurchaseTestInput purchaseTestInput = new PurchaseTestInput()
{
    IsWiretransfer = false,
    IsPromotionalPurchase = false,
    TotalPrice = 100,
    CreditCardNumber = "378734493671000"
};
            
RulesEvaluator rulesEvaluator = new RulesEvaluator();

rulesEvaluator.Eval(new PromotionalPurchaseRule(purchaseTestInput, this.PerformUIAssert));
rulesEvaluator.Eval(new CreditCardChargeRule(purchaseTestInput, 20, this.PerformUIAssert));
rulesEvaluator.OtherwiseEval(new PromotionalPurchaseRule(purchaseTestInput, this.PerformUIAssert));
rulesEvaluator.OtherwiseEval(new CreditCardChargeRule<CreditCardChargeRuleRuleResult>(purchaseTestInput, 30));
rulesEvaluator.OtherwiseEval(new CreditCardChargeRule<CreditCardChargeRuleAssertResult>(purchaseTestInput, 40));
rulesEvaluator.OtherwiseEval(new CreditCardChargeRule(purchaseTestInput, 50, this.PerformUIAssert));
rulesEvaluator.OtherwiseDo(() => Debug.WriteLine("Perform other UI actions"));          
            
rulesEvaluator.EvaluateRulesChains();

This is how the same conditional workflow looks like after the usage of Rules Design Pattern. As you can see, it is tremendously more readable than the first version.

The chain is evaluated once the EvaluateRulesChains method is called. The returned actions are executed in the order of execution of the configured rules. The action associated with a particular rule is executed only if the evaluation of the rules returns success otherwise it is skipped.

Rules Design Pattern Explained C# Code

All concrete rules classes should inherit from the base rule class.

C#
public abstract class BaseRule : IRule
{
    private readonly Action actionToBeExecuted;
    protected readonly RuleResult ruleResult;

    public BaseRule(Action actionToBeExecuted)
    {
        this.actionToBeExecuted = actionToBeExecuted;
        if (actionToBeExecuted != null)
        {
            this.ruleResult = new RuleResult(this.actionToBeExecuted);
        }
        else
        {
            this.ruleResult = new RuleResult();
        }
    }

    public BaseRule()
    {
        ruleResult = new RuleResult();
    }

    public abstract IRuleResult Eval();
}

It defines an abstract method that evaluates the current rule and holds the action that will be performed on success.

Here is how one concrete rule looks like:

C#
public class CreditCardChargeRule : BaseRule
{
    private readonly PurchaseTestInput purchaseTestInput;
    private readonly decimal totalPriceLowerBoundary;

    public CreditCardChargeRule(PurchaseTestInput purchaseTestInput, 
	decimal totalPriceLowerBoundary, Action actionToBeExecuted) 
        : base(actionToBeExecuted)
    {
        this.purchaseTestInput = purchaseTestInput;
        this.totalPriceLowerBoundary = totalPriceLowerBoundary;
    }
        
    public override IRuleResult Eval()
    {
        if (!string.IsNullOrEmpty(this.purchaseTestInput.CreditCardNumber) &&
            !this.purchaseTestInput.IsWiretransfer &&
            !this.purchaseTestInput.IsPromotionalPurchase &&
            this.purchaseTestInput.TotalPrice > this.totalPriceLowerBoundary)
        {
            this.ruleResult.IsSuccess = true;
            return this.ruleResult;
        }
        return new RuleResult();
    }
}

It can accept as many parameters and data as you need to perform the wrapped condition. It overrides the abstract Eval method where the prime condition is wrapped. If the condition is true, the IsSuccess property is set to true, and the positive rule result is returned. By positive outcome, I mean a result that holds the associated action, not an empty one.

Page Objects Who Make Code More Readable

The primary class for rules interpretation is the RulesEvaluator class.

C#
public class RulesEvaluator
{
    private readonly List<RulesChain> rules;

    public RulesEvaluator()
    {
        this.rules = new List<RulesChain>();
    }

    public RulesChain Eval(IRule rule)
    {
        var rulesChain = new RulesChain(rule);
        this.rules.Add(rulesChain);
        return rulesChain;
    }

    public void OtherwiseEval(IRule alternativeRule)
    {
        if (this.rules.Count == 0)
        {
            throw new ArgumentException("You cannot add ElseIf clause without If!");
        }
        this.rules.Last().ElseRules.Add(new RulesChain(alternativeRule));
    }

    public void OtherwiseDo(Action otherwiseAction)
    {
        if (this.rules.Count == 0)
        {
            throw new ArgumentException("You cannot add Else clause without If!");
        }
        this.rules.Last().ElseRules.Add(new RulesChain(new NullRule(otherwiseAction), true));
    }

    public void EvaluateRulesChains()
    {
        this.Evaluate(this.rules, false);
    }

    private void Evaluate(List<RulesChain> rulesToBeEvaluated, bool isAlternativeChain = false)
    {
        foreach (var currentRuleChain in rulesToBeEvaluated)
        {
            var currentRulesChainResult = currentRuleChain.Rule.Eval();
            if (currentRulesChainResult.IsSuccess)
            {
                currentRulesChainResult.Execute();
                if (isAlternativeChain)
                {
                    break;
                }
            }
            else
            {
                this.Evaluate(currentRuleChain.ElseRules, true);
            }
        }
    }
}

It provides methods for defining the IF, IF-ELSE and ELSE clauses. The IF is declared via Eval method, IF-ELSE through OtherwiseEval and ELSE with OtherwiseDo. Also, it holds the EvaluateRulesChains method that evaluates the entirely configured chain of conditions and executes all associated actions. It works internally with another class called RulesChain.

C#
public class RulesChain
{
    public IRule Rule { get; set; }

    public List<RulesChain> ElseRules { get; set; }

    public bool IsLastInChain { get; set; }

    public RulesChain(IRule mainRule, bool isLastInChain = false)
    {
        this.IsLastInChain = isLastInChain;
        this.ElseRules = new List<RulesChain>();
        this.Rule = mainRule;
    }
}

RulesChain represents a conditional workflow of IF, IF-ELSE and ELSE clauses. It holds the current rule (e.g. IF) and all following rules (e.g. IF-ELSE/ELSE).

Once the EvaluateRulesChains method is executed, the configured rules are evaluated consequently. If the Eval- rule returns success, all following OtherwiseEval and OtherwiseDo rules are skipped. If not, the next rule in the chain is evaluated and so on. The same pattern is applied as in the typical IF-IF-ELSE-ELSE workflow.

Rules Design Pattern Configuration

Rules Design Pattern Private Method Configuration

Private Methods Configuration

There are three types of configurations of rules, mainly related to the Action parameter of the BaseRule class.

C#
rulesEvaluator.Eval(new PromotionalPurchaseRule(purchaseTestInput, this.PerformUIAssert));

private void PerformUIAssert(string text = "Perform other UI actions")
{
    Debug.WriteLine(text);
}

The rule-associated action is defined as a private method in the class where the RulesEvaluater is configured. All actions associated with rules can be separated in different private methods.

Anonymous Method

Anonymous Method Configuration- Lambda Expression

Another way to pass the actions is via anonymous method using ? lambda expression.

C#
rulesEvaluator.Eval(new CreditCardChargeRule
	(purchaseTestInput, 20, () => Debug.WriteLine("Perform other UI actions")));
rulesEvaluator.Eval(new CreditCardChargeRule(purchaseTestInput, 20, () =>
{
    Debug.WriteLine("Perform other UI actions");
    Debug.WriteLine("Perform another UI action");
}));

In my opinion, this approach leads to unreadable code, so I stick to the first one.

Rules Design Pattern Generic Configuration

Generic Rule Result Configuration

You can create a generic, specific rule where the generic parameter represents a rule result where the associated action is declared. You can use different combinations of the rule and its results classes. However, this approach can lead to a class explosion so you should be careful.

C#
public class CreditCardChargeRule<TRuleResult> : BaseRule
    where TRuleResult : class, IRuleResult, new()
{
    private readonly PurchaseTestInput purchaseTestInput;
    private readonly decimal totalPriceLowerBoundary;

    public CreditCardChargeRule(PurchaseTestInput purchaseTestInput, decimal totalPriceLowerBoundary)
    {
        this.purchaseTestInput = purchaseTestInput;
        this.totalPriceLowerBoundary = totalPriceLowerBoundary;
    }

    public override IRuleResult Eval()
    {
        if (!string.IsNullOrEmpty(this.purchaseTestInput.CreditCardNumber) &&
            !this.purchaseTestInput.IsWiretransfer &&
            !this.purchaseTestInput.IsPromotionalPurchase &&
            this.purchaseTestInput.TotalPrice > this.totalPriceLowerBoundary)
        {
            this.ruleResult.IsSuccess = true;
            return this.ruleResult;
        }
        return new TRuleResult();
    }
}

This is how a sample concrete rule result class looks like.

C#
public class CreditCardChargeRuleAssertResult : IRuleResult
{
    public bool IsSuccess { get; set; }

    public void Execute()
    {
        Console.WriteLine("Perform DB asserts.");
    }
}

The usage is straightforward.

C#
rulesEvaluator.OtherwiseEval
(new CreditCardChargeRule<CreditCardChargeRuleRuleResult>(purchaseTestInput, 30));
rulesEvaluator.OtherwiseEval
(new CreditCardChargeRule<CreditCardChargeRuleAssertResult>(purchaseTestInput, 40));

The same rule is used twice with different actions wrapped in different result classes.

Summary

  • Consider using the Rules Design Pattern when you have a growing amount of conditional complexity.
  • Separate the logic of each rule and its effects into its class.
  • Divide the selection and processing of rules into a separate Evaluator class.

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

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

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

License

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