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