Rules design model in automation testing

0



as you probably know from my series ”

design patterns in automation testing

“I explain the benefits of applying design models in your automation projects.

in this article I will share with you the help you can get from using

rulers design template

. this can help you reduce the complexity of your conditional statements and reuse them as needed.

definition

separate the logic of each individual rule and its effects in its own class. separate rule selection and processing into a separate evaluator class.

  • separate individual rules from rule processing logic.

  • allow the addition of new rules without the need to modify the rest of the system.

abstract uml class diagram

image title

participants

the classes and objects participating in this model are:

  • I decide


    – defines the interface for all specific rules.
  • irulresult


    – defines the interface for the results of all specific rules.
  • rule of thumb


    – the base class provides basic functionality to all the rules that inherit from it.
  • to reign


    – the class represents a concrete implementation of the baserule class.
  • chain of rules


    – it is a helper class that contains the main rule for the current conditional statement and the rest of the conditional chain of rules.
  • rule evaluator


    – it is the main class which takes care of the creation of readable rules and their relation. it evaluates the rules and returns their results.

c # code rules design template

test case of the test

consider that we need to automate a shopping cart process. when purchasing, we can create orders by bank transfer, credit card or free orders through promotions. Our testing workflow is based on a purchase entry object that contains all data related to the current purchase, such as purchase type and 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; }
}

An example of a conditional test workflow for our test logic without any design patterns applied might 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 under the conditions can be: apply coupons or other promotions in the user interface, complete the order through different payment methods in the user interface, enforce different things in the user interface or the database. 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 very unreadable. also, another thing to consider is that you might need the same rules in different types of classes – you can use the rule once in a UI facade and a second time in a db assertion class.

enhanced version rules design template 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(purchasetestinput, 30));
rulesevaluator.otherwiseeval(new creditcardchargerule(purchasetestinput, 40));
rulesevaluator.otherwiseeval(new creditcardchargerule(purchasetestinput, 50, this.performuiassert));
rulesevaluator.otherwisedo(() => debug.writeline("perform other ui actions"));          

rulesevaluator.evaluateruleschains();

this is what the same conditional workflow looks like after using

rulers design template

. as you can see it is much more readable than the first version.

the string is evaluated after the

scoring rule chains

method is called. the returned actions are executed in the execution order of the configured rules. the action associated with a particular rule is only executed if the rule evaluation is successful otherwise it is ignored.

rules design template explained c # code

all concrete rule classes must 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 contains the action that will be taken on success.

this is what a 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 necessary to execute the encapsulated condition. it replaces the

abstract assessment

method where the

the first condition is packed

. if the condition is true, the

success

The property is set to true and the positive result of the rule is returned. by positive result, I mean a result that contains the associated action, not an empty result.

the main class for the interpretation of the rules is the

rule evaluator

to classify.

public class rulesevaluator
{
    private readonly list rules;

    public rulesevaluator()
    {
        this.rules = new list();
    }

    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 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 if, if-else, and else clauses. the if is declared via

assess

method, if-else through

otherwise

and if not with

otherwise

. also he holds the

scoring rule chains

method that evaluates the fully configured chain of conditions and performs all associated actions. it works internally with another class called

chain of rules

.

public class ruleschain
{
    public irule rule { get; set; }

    public list elserules { get; set; }

    public bool islastinchain { get; set; }

    public ruleschain(irule mainrule, bool islastinchain = false)
    {
        this.islastinchain = islastinchain;
        this.elserules = new list();
        this.rule = mainrule;
    }
}

chain of rules

represents a conditional workflow of if, if-else and else clauses. it contains the current rule (eg if) and all subsequent rules (eg if-else / else).

once there

scoring rule chains

method is executed, the configured rules are evaluated accordingly. if the

eval-

the rule returns success whatever follows otherwise the eval and sinondo rules are ignored. otherwise, 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.

rule design template configuration

configuration of private methods


there are three types of rule configurations, mainly related to the

action

parameter of the

rule of thumb

to classify.

rulesevaluator.eval(new promotionalpurchaserule(purchasetestinput, this.performuiassert));

private void performuiassert(string text = "perform other ui actions")
{
    debug.writeline(text);
}

the action associated with the rule is defined as a private method in the class where the rule evaluator is configured. all the actions associated with the rules can be separated into different private methods.

anonymous method configuration – lambda expression


another way to pass actions is to use an anonymous method using a 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 with the first one.

Generic rule result configuration


you can create a generic and 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 result classes. However, this approach can lead to a class explosion, so you need to be careful.

public class creditcardchargerule : 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 what an example of a concrete rule result class looks like.

public class creditcardchargeruleassertresult : iruleresult
{
    public bool issuccess { get; set; }

    public void execute()
    {
        console.writeline("perform db asserts.");
    }
}

the use is simple.

rulesevaluator.otherwiseeval(new creditcardchargerule(purchasetestinput, 30));
rulesevaluator.otherwiseeval(new creditcardchargerule(purchasetestinput, 40));

the same rule is used twice with different actions encapsulated in different result classes.

summary

  • Consider using the rule design model when you have increasing conditional complexity.

  • separate the logic of each rule and its effects in its class.

  • divide the selection and processing of rules into a separate class of evaluators.


Share.

Leave A Reply