Behaviours Design Pattern in Automated Testing

Behaviours Design Pattern in Automated Testing

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, are executed by a special engine.

Benefits

  • Increased tests’ readability

  • Improved tests’ maintainability

  • Flexible automated tests’ API

UML Class Diagram

classDiagram
    IBehaviour <|.. Behaviour
    Behaviour <|-- SignInPageLoginBehaviour
    Behaviour <|-- ItemPageNavigationBehaviour
    BehaviourEngine ..> Behaviour
    SignInPageLoginBehaviour ..> SignInPage
    SignInPageLoginBehaviour ..> ShippingAddressPage
    SignInPageLoginBehaviour ..> PurchaseTestContext
    ItemPageNavigationBehaviour ..> PurchaseTestContext
    ItemPageNavigationBehaviour ..> ItemPage
    class IBehaviour {
        <<interface>>
        +PerformAct()
        +PerformPostAct()
        +PerformPostActAsserts()
        +PerformPreActAsserts()
    }
    class Behaviour {
        +PerformAct()
        +PerformPostAct()
        +PerformPostActAsserts()
        +PerformPreActAsserts()
    }
    class BehaviourEngine {
        +Execute(params Type[] pageBehaviours)
    }
    class SignInPageLoginBehaviour {
        +PerformAct()
        +PerformPostAct()
    }
    class ItemPageNavigationBehaviour {
        +PerformAct()
    }
    class PurchaseTestContext {
        +ClientLoginInfo
        +ClientPurchaseInfo
        +ItemPrice
        +ItemUrl
    }
    class SignInPage {
        +Login()
    }
    class ShippingAddressPage {
        +WaitForPageToLoad()
    }
    class ItemPage {
        +Navigate()
    }

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

    he 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 going to automate is once again an online purchase process. 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.

For more detailed overview and usage of many more design patterns and best practices in automated testing, check my book “Design Patterns for High-Quality Automated Tests, C# Edition, High-Quality Tests Attributes, and Best Practices”.  You can read part of three of the chapters:

Defining High-Quality Test Attributes for Automated Tests

Benchmarking for Assessing Automated Test Components Performance

Generic Repository Design Pattern- Test Data Preparation

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


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, T3, T4>()
    where T1 : Behaviour
    where T2 : Behaviour
    where T3 : Behaviour
    where T4 : Behaviour
    {
        Execute(typeof(T1), typeof(T2), typeof(T3), typeof(T4));
    }
    // contains 15 more overloads...
    public static void Execute<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20>()
    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


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<ТBehavior>(
    BehaviourActions behaviourAction,
    Action action)
    where ТBehavior : IBehaviour
    {
        if (!this.overridenBehavioursActions.ContainsKey(typeof(ТBehavior)))
        {
            this.overridenBehavioursActions.Add(
            typeof(ТBehavior),
            new Dictionary<BehaviourActions, Action>());
        }
        if (!this.overridenBehavioursActions[typeof(ТBehavior)].ContainsKey(
        behaviourAction))
        {
            this.overridenBehavioursActions[typeof(ТBehavior)].Add(behaviourAction, action);
        }
        else
        {
            this.overridenBehavioursActions[typeof(ТBehavior)] = 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()].Invoke();
        }
        else
        {
            defaultBehaviourOperation.Invoke();
        }
    }
}

Above is the code of the extended behaviour engine that can contain overridden parts. 

Overridden Actions Behaviour Engine in Tests


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,
    () =>
    {
        // wait for different URL for this case.
    });
    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<ТBehavior>(
    BehaviourActions behaviourAction,
    Action action)
    where ТBehavior : IBehaviour
    {
        if (!this.overridenBehavioursActions.ContainsKey(typeof(ТBehavior)))
        {
            this.overridenBehavioursActions.Add(typeof(ТBehavior),
            new Dictionary<BehaviourActions, Action>());
        }
        if (!this.overridenBehavioursActions[typeof(ТBehavior)].ContainsKey(behaviourAction))
        {
            this.overridenBehavioursActions[typeof(ТBehavior)].Add(behaviourAction, action);
        }
        else
        {
            this.overridenBehavioursActions[typeof(ТBehavior)] = 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()].Invoke();
        }
        else
        {
            defaultBehaviourOperation.Invoke();
        }
    }
}

Instead of the Activator class, the Unity IoC container resolves the behaviours.

Unity Behaviour Engine in Tests


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,
    () =>
    {
        // wait for different URL for this case.
    });
    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.

Related Articles

Design Patterns

Proxy Design Pattern in Automated Testing

Achieving high-quality test automation that brings value- you need to understand core programming concepts such as SOLID and the usage of design patterns. In th

Proxy Design Pattern in Automated Testing

Design Patterns

Generic Repository Design Pattern- Test Data Preparation

Often we can run the tests against an empty DB. However, we still need initial data. We can generate it ourselves. To do so, we need to add a code for accessing

Generic Repository Design Pattern- Test Data Preparation

Design Patterns

Facade Design Pattern in Automated Testing

An object that provides a simplified interface to a larger body of code, such as class library. Make a software library easier to use, understand and more reada

Facade Design Pattern in Automated Testing

Design Patterns

Specification Design Pattern in Automated Testing

If you follow my series about Design Patterns in Automated Testing, I explain how you can utilize the power of various design patterns in your tests. In the cur

Specification Design Pattern in Automated Testing

Design Patterns

Page Objects- Partial Classes Base Pages- WebDriver C#

Editorial Note: I originally wrote this post for the Test Huddle Blog. You can check out the original here, at their site.

Page Objects- Partial Classes Base Pages- WebDriver C#

Design Patterns

Observer Design Pattern Classic Implementation in Automated Testing

The Observer Design Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updat

Observer Design Pattern Classic Implementation in Automated Testing
Anton Angelov

About the author

Anton Angelov is Managing Director, Co-Founder, and Chief Test Automation Architect at Automate The Planet — a boutique consulting firm specializing in AI-augmented test automation strategy, implementation, and enablement. He is the creator of BELLATRIX, a cross-platform framework for web, mobile, desktop, and API testing, and the author of 8 bestselling books on test automation. A speaker at 60+ international conferences and researcher in AI-driven testing and LLM-based automation, he has been recognized as QA of the Decade and Webit Changemaker 2025.