Introduction
I think it is time to stop informing you that this is the newest edition to the most popular series- Design Patterns in Automated Testing. The so called by me Behaviours Design Pattern is a design pattern that you won't find in the list of all official design patterns. But as you know, the design pattern is a general repeatable solution to a commonly occurring problem in software design. Behaviours Design Pattern increases the flexibility of your automated tests' API. You can create tests in the same manner as if you build LEGO bricks.
Definition
The behaviours are small reusable workflows. They are combined in different tests. Usually, they are executed by a special engine.
- Flexible automated tests' API
- Improved tests' maintainability
- Increased tests' readability
UML Class Diagram
Participants
The classes and objects participating in Behaviours Design Pattern are:
IBehaviour
- Defines the interfaces for all behaviours. Contains all methods that one behaviour can override. Behaviour
- The base class for all behaviours implements IBehaviour
interface and holds empty virtual implementations of all methods. PurchaseTestContext
- Static
class that contains all relevant information for single purchase related test like URL, prices and discounts. ItemPageNavigationBehaviour
- A concrete behaviour for the ItemPage
class. It holds a logic for navigation. ItemPage
- A concrete page object that provides different service operations that can be performed on the page. It is used in the concrete behaviours. BehaviourEngine
- It is the class that executes a list of behaviours' workflows.
Behaviours Design Pattern C# Code
Behaviours Design Pattern is a pattern that we discovered with my teammates during a period when we were searching for a better system tests' design. You won't find it in the books. A funny fact, when my manager first came up with this idea he named the pattern- Lego Design Pattern because you build the system tests in the same manner when you construct LEGO.
Previously, we had really huge tests that contain massive workflows. However, often we had to customise them. In the past system tests' design, we used the Facade Design Pattern. So for every custom workflow, we needed to have different facade method. This led to enormous files and less maintainable and not so flexible tests.The main idea behind the Behaviours Design Pattern is that the huge workflow is split in multiple meaningful smaller workflows called behaviours. For example, the login can be a separate routine.
IBehaviour Interface
public interface IBehaviour
{
void PerformAct();
void PerformPostAct();
void PerformPostActAsserts();
void PerformPreActAsserts();
}
The IBehaviour
interface defines the small workflow. You can perform an action than wait for something to happen in the PostAct phase. After that, you can assert the state of the page. Also, you can verify the state before the initial action.
Base Behaviour
public class Behaviour : IBehaviour
{
public virtual void PerformAct()
{
}
public virtual void PerformPostAct()
{
}
public virtual void PerformPostActAsserts()
{
}
public virtual void PerformPreActAsserts()
{
}
}
Behaviour
implements the IBehaviour
interface. It only contains empty virtual methods. If the small concrete workflow doesn't have some of the operations, it doesn't override them.
Concrete Behaviours
public class ItemPageBuyBehaviour : Behaviour
{
private readonly ItemPage itemPage;
public ItemPageBuyBehaviour(ItemPage itemPage)
{
this.itemPage = itemPage;
}
public override void PerformAct()
{
this.itemPage.ClickBuyNowButton();
}
}
The test case that I am automating is once again an Amazon online purchase. You can find the full test case description in my article about the Null Object Design Pattern. The behaviour contains a reference to the related page object - ItemPage
. An initialized instance of the page is passed to the constructor. This is a simple workflow because it holds only a single action. However, there are cases where you can have multiple actions inside your behaviours. For example, applying different discounts or asserting many taxes.
public class SignInPageLoginBehaviour : Behaviour
{
private readonly SignInPage signInPage;
private readonly ShippingAddressPage shippingAddressPage;
public SignInPageLoginBehaviour(
SignInPage signInPage,
ShippingAddressPage shippingAddressPage)
{
this.signInPage = signInPage;
this.shippingAddressPage = shippingAddressPage;
}
public override void PerformAct()
{
this.signInPage.Login(
PurchaseTestContext.ClientLoginInfo.Email,
PurchaseTestContext.ClientLoginInfo.Password);
}
public override void PerformPostAct()
{
this.shippingAddressPage.WaitForPageToLoad();
}
}
This is a more complicated example. The main action logs in the client then wait for the next page to load. The tests' related data is passed through the static
class PurchaseTestContext
.
Simple Behaviour Engine
public static class SimpleBehaviourEngine
{
public static void Execute(params Type[] pageBehaviours)
{
foreach (Type pageBehaviour in pageBehaviours)
{
var currentbehaviour = Activator.CreateInstance(pageBehaviour) as Behaviour;
currentbehaviour.PerformPreActAsserts();
currentbehaviour.PerformAct();
currentbehaviour.PerformPostActAsserts();
currentbehaviour.PerformPostAct();
}
}
}
Above, you can find the simplest implementation of BehaviourEngine
. We pass the ordered list of behaviours for the current tests' test case. Then the behaviours are created via the Activator
class. After that, their actions are performed in expected workflow's order.
Simple Behaviour Engine in Tests
[TestMethod]
public void Purchase_SimpleBehaviourEngine()
{
PurchaseTestContext.ItemUrl = "/Selenium-Testing-Cookbook-Gundecha-Unmesh/dp/1849515743";
PurchaseTestContext.ItemPrice = "40.49";
PurchaseTestContext.ClientPurchaseInfo = new ClientPurchaseInfo(
new ClientAddressInfo()
{
FullName = "John Smith",
Country = "United States",
Address1 = "950 Avenue of the Americas",
State = "New York",
City = "New York City",
Zip = "10001-2121",
Phone = "00164644885569"
});
PurchaseTestContext.ClientPurchaseInfo.CouponCode = "99PERDIS";
PurchaseTestContext.ClientLoginInfo = new ClientLoginInfo()
{
Email = "g3984159@trbvm.com",
Password = "ASDFG_12345"
};
SimpleBehaviourEngine.Execute(
typeof(ItemPageNavigationBehaviour),
typeof(ItemPageBuyBehaviour),
typeof(PreviewShoppingCartPageProceedBehaviour),
typeof(SignInPageLoginBehaviour),
typeof(ShippingAddressPageFillShippingBehaviour),
typeof(ShippingAddressPageFillDifferentBillingBehaviour),
typeof(ShippingAddressPageContinueBehaviour),
typeof(ShippingPaymentPageContinueBehaviour),
typeof(PlaceOrderPageAssertFinalAmountsBehaviour));
}
First, you need to initialize the static PurchaseTestContext
class. After that, you call the Execute
method of SimpleBehaviourEngine
. The desired test's workflow is passed as an ordered list of behaviour classes' types.
Generic Behaviour Engine
public static class GenericBehaviourEngine
{
public static void Execute(params Type[] pageBehaviours)
{
foreach (Type pageBehaviour in pageBehaviours)
{
var currentbehaviour = Activator.CreateInstance(pageBehaviour) as Behaviour;
currentbehaviour.PerformPreActAsserts();
currentbehaviour.PerformAct();
currentbehaviour.PerformPostActAsserts();
currentbehaviour.PerformPostAct();
}
}
public static void Execute<t1>()
where T1 : Behaviour
{
Execute(typeof(T1));
}
public static void Execute<t1, t2="">()
where T1 : Behaviour
where T2 : Behaviour
{
Execute(typeof(T1), typeof(T2));
}
public static void Execute<t1, t2="" t3="">()
where T1 : Behaviour
where T2 : Behaviour
where T3 : Behaviour
{
Execute(typeof(T1), typeof(T2), typeof(T3));
}
public static void Execute<t1, t2="">()
where T1 : Behaviour
where T2 : Behaviour
where T3 : Behaviour
where T4 : Behaviour
{
Execute(typeof(T1), typeof(T2), typeof(T3), typeof(T4));
}
public static void Execute<t1, t2="">()
where T1 : Behaviour
where T2 : Behaviour
where T3 : Behaviour
where T4 : Behaviour
where T5 : Behaviour
where T6 : Behaviour
where T7 : Behaviour
where T8 : Behaviour
where T9 : Behaviour
where T10 : Behaviour
where T11 : Behaviour
where T12 : Behaviour
where T13 : Behaviour
where T14 : Behaviour
where T15 : Behaviour
where T16 : Behaviour
where T17 : Behaviour
where T18 : Behaviour
where T19 : Behaviour
where T20 : Behaviour
{
Execute(
typeof(T1),
typeof(T2),
typeof(T3),
typeof(T4),
typeof(T5),
typeof(T6),
typeof(T7),
typeof(T8),
typeof(T9),
typeof(T10),
typeof(T12),
typeof(T13),
typeof(T14),
typeof(T15),
typeof(T16),
typeof(T17),
typeof(T18),
typeof(T19),
typeof(T20),
typeof(T11));
}
}
It is basically the same class plus additional 20 overloaded generic methods. Instead of passing all types using the typeof
operator, you can use these generic methods.
Generic Behaviour Engine in Tests
[TestMethod]
public void Purchase_GenericBehaviourEngine()
{
PurchaseTestContext.ItemUrl = "/Selenium-Testing-Cookbook-Gundecha-Unmesh/dp/1849515743";
PurchaseTestContext.ItemPrice = "40.49";
PurchaseTestContext.ClientPurchaseInfo = new ClientPurchaseInfo(
new ClientAddressInfo()
{
FullName = "John Smith",
Country = "United States",
Address1 = "950 Avenue of the Americas",
State = "New York",
City = "New York City",
Zip = "10001-2121",
Phone = "00164644885569"
});
PurchaseTestContext.ClientPurchaseInfo.CouponCode = "99PERDIS";
PurchaseTestContext.ClientLoginInfo = new ClientLoginInfo()
{
Email = "g3984159@trbvm.com",
Password = "ASDFG_12345"
};
GenericBehaviourEngine.Execute<
ItemPageNavigationBehaviour,
ItemPageBuyBehaviour,
PreviewShoppingCartPageProceedBehaviour,
SignInPageLoginBehaviour,
ShippingAddressPageFillShippingBehaviour,
ShippingAddressPageFillDifferentBillingBehaviour,
ShippingAddressPageContinueBehaviour,
ShippingPaymentPageContinueBehaviour,
PlaceOrderPageAssertFinalAmountsBehaviour>();
}
It requires less code to configure the test's workflow.
Override Part of the Small Workflows
Sometimes, it comes in handy if you can override just a small part of the workflow. There are cases where you need to create a test for some less common scenario, so you don't want to create a separate reusable behaviour.
public class OverridenActionsBehaviourEngine
{
private readonly Dictionary<Type, Dictionary<BehaviourActions, Action>>
overridenBehavioursActions;
public OverridenActionsBehaviourEngine()
{
this.overridenBehavioursActions =
new Dictionary<Type, Dictionary<BehaviourActions, Action>>();
}
public void Execute(params Type[] pageBehaviours)
{
foreach (Type pageBehaviour in pageBehaviours)
{
var currentbehaviour = Activator.CreateInstance(pageBehaviour) as Behaviour;
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.PreActAsserts,
() => currentbehaviour.PerformPreActAsserts());
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.Act,
() => currentbehaviour.PerformAct());
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.PostActAsserts,
() => currentbehaviour.PerformPostActAsserts());
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.PostAct,
() => currentbehaviour.PerformPostAct());
}
}
public void ConfugureCustomBehaviour<TBehavior>(
BehaviourActions behaviourAction,
Action action)
where TBehavior : IBehaviour
{
if (!this.overridenBehavioursActions.ContainsKey(typeof(TBehavior)))
{
this.overridenBehavioursActions.Add(
typeof(TBehavior),
new Dictionary<BehaviourActions, Action>());
}
if (!this.overridenBehavioursActions[typeof(TBehavior)].ContainsKey(
behaviourAction))
{
this.overridenBehavioursActions[typeof(TBehavior)].Add(behaviourAction, action);
}
else
{
this.overridenBehavioursActions[typeof(TBehavior)][behaviourAction] = action;
}
}
private void ExecuteBehaviourOperation(
Type pageBehaviour,
BehaviourActions behaviourAction,
Action defaultBehaviourOperation)
{
if (this.overridenBehavioursActions.ContainsKey(pageBehaviour.GetType()) &&
this.overridenBehavioursActions[pageBehaviour.GetType()].ContainsKey(
behaviourAction))
{
this.overridenBehavioursActions[pageBehaviour.GetType()][behaviourAction].Invoke();
}
else
{
defaultBehaviourOperation.Invoke();
}
}
}
Above is the code of the extended behaviour engine that can contain overridden parts.
Overridden Actions Behaviour Engine in Tests
[TestMethod]
public void Purchase_OverridenActionsBehaviourEngine()
{
PurchaseTestContext.ItemUrl = "/Selenium-Testing-Cookbook-Gundecha-Unmesh/dp/1849515743";
PurchaseTestContext.ItemPrice = "40.49";
PurchaseTestContext.ClientPurchaseInfo = new ClientPurchaseInfo(
new ClientAddressInfo()
{
FullName = "John Smith",
Country = "United States",
Address1 = "950 Avenue of the Americas",
State = "New York",
City = "New York City",
Zip = "10001-2121",
Phone = "00164644885569"
});
PurchaseTestContext.ClientPurchaseInfo.CouponCode = "99PERDIS";
PurchaseTestContext.ClientLoginInfo = new ClientLoginInfo()
{
Email = "g3984159@trbvm.com",
Password = "ASDFG_12345"
};
var behaviourEngine = new OverriddenActionsBehaviourEngine();
behaviourEngine.ConfugureCustomBehaviour<signinpageloginbehaviour>(
BehaviourActions.PostAct,
() => {
});
behaviourEngine.Execute(
typeof(ItemPageNavigationBehaviour),
typeof(ItemPageBuyBehaviour),
typeof(PreviewShoppingCartPageProceedBehaviour),
typeof(SignInPageLoginBehaviour),
typeof(ShippingAddressPageFillShippingBehaviour),
typeof(ShippingAddressPageFillDifferentBillingBehaviour),
typeof(ShippingAddressPageContinueBehaviour),
typeof(ShippingPaymentPageContinueBehaviour),
typeof(PlaceOrderPageAssertFinalAmountsBehaviour));
}
The test is almost identical to the previous ones except the ConfigureCustomBehaviour
method. There, you set the type of the behaviour you want to override. You don't replace the whole workflow, only part of it, so you need to configure which is the part. Lastly, you pass an anonymous action that is going to be executed instead of the default action.
Improve Behaviour Engine with Unity IoC Container
public class UnityBehaviourEngine
{
private readonly IUnityContainer unityContainer;
private readonly Dictionary<Type, Dictionary<BehaviourActions, Action>>
overridenBehavioursActions;
public UnityBehaviourEngine(IUnityContainer unityContainer)
{
this.unityContainer = unityContainer;
this.overridenBehavioursActions =
new Dictionary<Type, Dictionary<BehaviourActions, Action>>();
}
public void Execute(params Type[] pageBehaviours)
{
foreach (Type pageBehaviour in pageBehaviours)
{
var currentbehaviour = this.unityContainer.Resolve(pageBehaviour) as Behaviour;
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.PreActAsserts,
() => currentbehaviour.PerformPreActAsserts());
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.Act,
() => currentbehaviour.PerformAct());
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.PostActAsserts,
() => currentbehaviour.PerformPostActAsserts());
this.ExecuteBehaviourOperation(
pageBehaviour,
BehaviourActions.PostAct,
() => currentbehaviour.PerformPostAct());
}
}
public void ConfugureCustomBehaviour<TBehavior>(
BehaviourActions behaviourAction,
Action action)
where TBehavior : IBehaviour
{
if (!this.overridenBehavioursActions.ContainsKey(typeof(TBehavior)))
{
this.overridenBehavioursActions.Add(typeof(TBehavior),
new Dictionary<BehaviourActions, Action>());
}
if (!this.overridenBehavioursActions[typeof(TBehavior)].ContainsKey(behaviourAction))
{
this.overridenBehavioursActions[typeof(TBehavior)].Add(behaviourAction, action);
}
else
{
this.overridenBehavioursActions[typeof(TBehavior)][behaviourAction] = action;
}
}
private void ExecuteBehaviourOperation(
Type pageBehaviour,
BehaviourActions behaviourAction,
Action defaultBehaviourOperation)
{
if (this.overridenBehavioursActions.ContainsKey(pageBehaviour.GetType()) &&
this.overridenBehavioursActions[pageBehaviour.GetType()].ContainsKey(
behaviourAction))
{
this.overridenBehavioursActions[pageBehaviour.GetType()][behaviourAction].Invoke();
}
else
{
defaultBehaviourOperation.Invoke();
}
}
}
Instead of the Activator
class, the Unity IoC container resolves the behaviours.
Unity Behaviour Engine in Tests
[TestMethod]
public void Purchase_UnityBehaviourEngine()
{
PurchaseTestContext.ItemUrl = "/Selenium-Testing-Cookbook-Gundecha-Unmesh/dp/1849515743";
PurchaseTestContext.ItemPrice = "40.49";
PurchaseTestContext.ClientPurchaseInfo = new ClientPurchaseInfo(
new ClientAddressInfo()
{
FullName = "John Smith",
Country = "United States",
Address1 = "950 Avenue of the Americas",
State = "New York",
City = "New York City",
Zip = "10001-2121",
Phone = "00164644885569"
});
PurchaseTestContext.ClientPurchaseInfo.CouponCode = "99PERDIS";
PurchaseTestContext.ClientLoginInfo = new ClientLoginInfo()
{
Email = "g3984159@trbvm.com",
Password = "ASDFG_12345"
};
var behaviourEngine = new UnityBehaviourEngine(container);
behaviourEngine.ConfugureCustomBehaviour<signinpageloginbehaviour>(
BehaviourActions.PostAct,
() => {
});
behaviourEngine.Execute(
typeof(ItemPageNavigationBehaviour),
typeof(ItemPageBuyBehaviour),
typeof(PreviewShoppingCartPageProceedBehaviour),
typeof(SignInPageLoginBehaviour),
typeof(ShippingAddressPageFillShippingBehaviour),
typeof(ShippingAddressPageFillDifferentBillingBehaviour),
typeof(ShippingAddressPageContinueBehaviour),
typeof(ShippingPaymentPageContinueBehaviour),
typeof(PlaceOrderPageAssertFinalAmountsBehaviour));
}
The usage in tests is absolutely identical to the previous one with the only different that you need to register all related types in the container.
Design Patterns in Automated Testing
CodeProject
The post Behaviours Design Pattern in Automated Testing appeared first on Automate The Planet.