Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Rules Design Pattern in Automation Testing

DZone's Guide to

Rules Design Pattern in Automation Testing

Working on testing automation? Here's a run down of design pattern rules, including definitions and examples.

· DevOps Zone
Free Resource

The Nexus Suite is uniquely architected for a DevOps native world and creates value early in the development pipeline, provides precise contextual controls at every phase, and accelerates DevOps innovation with automation you can trust. Read how in this ebook.

Rules Design Pattern in Automation Testing

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.

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

Image title

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.

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.

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

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.

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.

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.

 The primary class for rules interpretation is the RulesEvaluator class.

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.

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

Private Methods Configuration

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

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 Configuration- Lambda Expression

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

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.

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.

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.

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

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

The usage is straightforward.

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.

The DevOps Zone is brought to you in partnership with Sonatype Nexus.  See how the Nexus platform infuses precise open source component intelligence into the DevOps pipeline early, everywhere, and at scale. Read how in this ebook

Topics:
testing ,rules ,design pattern ,automated testing

Published at DZone with permission of Anton Angelov, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}