Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Create Hybrid Test Framework – Dynamically Configure Execution Engine

4.75/5 (3 votes)
7 Aug 2016Ms-PL4 min read 6.8K  
Build a Hybrid Test Automation Framework. Learn how to switch between execution engines and browsers through attributes and Observer Design Pattern.

Introduction

In this article from the Design & Architecture Series, I am going to explain how to build a flexible way of configuring the current execution engine and browser. I am going to utilize the power of the Observer Design Pattern, C# reflection and attributes.

Hybrid Test Framework- Configure Execution Engine via Observer Design Pattern

Class Diagram

Image 1

As pointed at the beginning of the post, I am going to use the Observer Design Pattern in the implementation of the dynamic configuration of the test framework's engine. I suggest reading the whole article about it to understand the current solution thoroughly.

New Behaviours Project

Image 2

If you read my articles dedicated to the Observer Design Pattern, you know that you can utilize it in many ways so that your test framework can benefit. Because of that, I created a separate project where the additional extendable logic will be placed.

Execution Engine Attribute

C#
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, 
AllowMultiple = false)]
public class ExecutionEngineAttribute : Attribute
{
    public ExecutionEngineAttribute(ExecutionEngineType executionEngineType, Browsers browser)
    {
        this.Browser = browser;
        this.ExecutionEngineType = executionEngineType;
    }

    public ExecutionEngineAttribute(Browsers browser)
    {
        this.Browser = browser;
        this.ExecutionEngineType = ExecutionEngineType.TestStudio;
    }

    public Browsers Browser { get; set; }

    public ExecutionEngineType ExecutionEngineType { get; set; }
}

The main way of changing the execution engine and its browser will be through the ExecutionEngineAttribute. It holds properties to the type of the execution engine (WebDriver or Testing Framework) and the browser type. You can place it on a class level or change the engine only for a particular test method.

ExecutionEngineBehaviorObserver

The heart of the engine's configuration logic is the ExecutionEngineBehaviourObserver. Here on PreTestInit, the current driver is resolved. Through the method GetExecutionEngineType, we get the engine configuration specified in the class body. We pass the MethodInfo instance through the event arguments. Two other separate private methods extract the ExecutionEngineAttributes values from the class attribute or test method attributes. The same logic is applied for the browser through the ConfigureTestExecutionBrowser method.

C#
public class ExecutionEngineBehaviorObserver : BaseTestBehaviorObserver
{
    private readonly IUnityContainer unityContainer;
    private ExecutionEngineType executionEngineType;
    private Browsers executionBrowserType;
    private IDriver driver;

    public ExecutionEngineBehaviorObserver(IUnityContainer unityContainer)
    {
        this.unityContainer = unityContainer;
    }

    protected override void PostTestCleanup(object sender, 
    HybridTestFramework.UITests.Core.Behaviours.TestExecutionEventArgs e)
    {
        this.driver.Quit();
    }

    protected override void PreTestInit(object sender, 
    HybridTestFramework.UITests.Core.Behaviours.TestExecutionEventArgs e)
    {
        this.executionBrowserType = this.ConfigureTestExecutionBrowser(e.MemberInfo);
        this.executionEngineType = this.GetExecutionEngineType(e.MemberInfo);
        this.ResolveAllDriverDependencies();
    }

    private ExecutionEngineType GetExecutionEngineTypeByMethodInfo(MemberInfo memberInfo)
    {
        if (memberInfo == null)
        {
            throw new ArgumentNullException("The test method's info cannot be null.");
        }

        var executionEngineTypeMethodAttribute = 
        memberInfo.GetCustomAttribute<ExecutionEngineAttribute>();
        if (executionEngineTypeMethodAttribute != null)
        {
            return executionEngineTypeMethodAttribute.ExecutionEngineType;
        }
        return ExecutionEngineType.NotSpecified;
    }

    private ExecutionEngineType GetExecutionEngineType(Type currentType)
    {
        if (currentType == null)
        {
            throw new ArgumentNullException("The test method's type cannot be null.");
        }

        var executionEngineClassAttribute = 
              currentType.GetCustomAttribute<ExecutionEngineAttribute>(true);
        if (executionEngineClassAttribute != null)
        {
            return executionEngineClassAttribute.ExecutionEngineType;
        }
        return ExecutionEngineType.NotSpecified;
    }

    private ExecutionEngineType GetExecutionEngineType(MemberInfo memberInfo)
    {
        var executionEngineType = ExecutionEngineType.TestStudio;

        ExecutionEngineType methodExecutionEngineType = 
        this.GetExecutionEngineTypeByMethodInfo(memberInfo);
        ExecutionEngineType classExecutionEngineType = 
        this.GetExecutionEngineType(memberInfo.DeclaringType);

        if (methodExecutionEngineType != ExecutionEngineType.NotSpecified)
        {
            executionEngineType = methodExecutionEngineType;
        }
        else if (classExecutionEngineType != ExecutionEngineType.NotSpecified)
        {
            executionEngineType = classExecutionEngineType;
        }

        return executionEngineType;
    }

    private void ResolveAllDriverDependencies()
    {
        var browserSettings = new BrowserSettings(this.executionBrowserType);
        if (this.executionEngineType.Equals(ExecutionEngineType.TestStudio))
        { 
            this.unityContainer.RegisterType<IDriver, TestingFrameworkDriver>(
                new InjectionFactory(x => 
                new TestingFrameworkDriver(this.unityContainer, browserSettings)));
            this.driver = this.unityContainer.Resolve<IDriver>();

            this.unityContainer.RegisterType<IButton, TestingFrameworkControls.Button>();
            this.unityContainer.RegisterType<ITextBox, TestingFrameworkControls.TextBox>();
            this.unityContainer.RegisterType<IDiv, TestingFrameworkControls.Div>();
            this.unityContainer.RegisterType<ISearch, TestingFrameworkControls.Search>();
            this.unityContainer.RegisterType<IInputSubmit, TestingFrameworkControls.InputSubmit>();
        }
        else if (this.executionEngineType.Equals(ExecutionEngineType.WebDriver))
        {
            this.unityContainer.RegisterType<IDriver, SeleniumDriver>(
                new InjectionFactory(x => new SeleniumDriver(this.unityContainer, browserSettings)));
            this.driver = this.unityContainer.Resolve<IDriver>();

            this.unityContainer.RegisterType<IButton, SeleniumControls.Button>();
            this.unityContainer.RegisterType<ITextBox, SeleniumControls.TextBox>();
            this.unityContainer.RegisterType<IDiv, SeleniumControls.Div>();
            this.unityContainer.RegisterType<ISearch, SeleniumControls.Search>();
            this.unityContainer.RegisterType<IInputSubmit, SeleniumControls.InputSubmit>();
        }

        this.unityContainer.RegisterInstance<IDriver>(this.driver);
        this.unityContainer.RegisterInstance<IBrowser>(this.driver);
        this.unityContainer.RegisterInstance<ICookieService>(this.driver);
        this.unityContainer.RegisterInstance<IDialogService>(this.driver);
        this.unityContainer.RegisterInstance<IJavaScriptInvoker>(this.driver);
        this.unityContainer.RegisterInstance<INavigationService>(this.driver);
        this.unityContainer.RegisterInstance<IElementFinder>(this.driver);
    }

    private Browsers ConfigureTestExecutionBrowser(MemberInfo memberInfo)
    {
        var currentExecutionBrowserType = Browsers.Firefox;
        Browsers methodExecutionBrowser = this.GetExecutionBrowser(memberInfo);
        Browsers classExecutionBrowser = this.GetExecutionBrowser(memberInfo.DeclaringType);

        if (methodExecutionBrowser != Browsers.NotSet)
        {
            currentExecutionBrowserType = methodExecutionBrowser;
        }
        else
        {
            if (classExecutionBrowser != Browsers.NotSet)
            {
                currentExecutionBrowserType = classExecutionBrowser;
            }
            else
            {
                currentExecutionBrowserType = Browsers.InternetExplorer;
            }
        }

        return currentExecutionBrowserType;
    }

    private Browsers GetExecutionBrowser(MemberInfo memberInfo)
    {
        if (memberInfo == null)
        {
            throw new ArgumentNullException("The test method's info cannot be null.");
        }

        var executionBrowserAttribute = 
        memberInfo.GetCustomAttribute<ExecutionEngineAttribute>(true);
        if (executionBrowserAttribute != null)
        {
            return executionBrowserAttribute.Browser;
        }
        return Browsers.NotSet;
    }

    private Browsers GetExecutionBrowser(Type currentType)
    {
        if (currentType == null)
        {
            throw new ArgumentNullException("The test method's type cannot be null.");
        }

        var executionBrowserAttribute = 
        currentType.GetCustomAttribute<ExecutionEngineAttribute>(true);
        if (executionBrowserAttribute != null)
        {
            return executionBrowserAttribute.Browser;
        }
        return Browsers.NotSet;
    }
}

ResolveAllDriverDependencies Method

The ResolveAllDriverDependencies is the most prominent member of the ExecutionEngineBehaviourObserver class. Inside, all of the magic is happening. This code is going to be executed again and again for every test in its PreTestInit phase. Based on the type of the currently configured execution engine, we override the type registrations of the driver's engine in the Unity container. First, we register as a type the IDriver interface. The interesting part is that we need to use the Unity InjectionFactory to configure it. When we resolve the IDriver interface, this factory will be executed. We need this because the TestingFrameworkDriver's constructor requires two parameters which cannot be passed using the default syntax. After that, we override the type registrations for all controls' interfaces so that they will be resolved for the correct engine- in this case, for the TestingFrameworkDriver. At the end of the method, we register as instances all driver's interfaces. The next time when we try to resolve one of them, the instance passed to the RegisterInstance method will be returned. If we skip this part, every time you resolve some of these interfaces, a separate browser will be created.

C#
private void ResolveAllDriverDependencies()
{
    var browserSettings = new BrowserSettings(this.executionBrowserType);
    if (this.executionEngineType.Equals(ExecutionEngineType.TestStudio))
    { 
        this.unityContainer.RegisterType<IDriver, TestingFrameworkDriver>(
            new InjectionFactory(x => 
            new TestingFrameworkDriver(this.unityContainer, browserSettings)));
        this.driver = this.unityContainer.Resolve<IDriver>();

        this.unityContainer.RegisterType<IButton, TestingFrameworkControls.Button>();
        this.unityContainer.RegisterType<ITextBox, TestingFrameworkControls.TextBox>();
        this.unityContainer.RegisterType<IDiv, TestingFrameworkControls.Div>();
        this.unityContainer.RegisterType<ISearch, TestingFrameworkControls.Search>();
        this.unityContainer.RegisterType<IInputSubmit, TestingFrameworkControls.InputSubmit>();
    }
    else if (this.executionEngineType.Equals(ExecutionEngineType.WebDriver))
    {
        // WebDriver Dependencies Resolved
    }

    this.unityContainer.RegisterInstance<IDriver>(this.driver);
    this.unityContainer.RegisterInstance<IBrowser>(this.driver);
    this.unityContainer.RegisterInstance<ICookieService>(this.driver);
    this.unityContainer.RegisterInstance<IDialogService>(this.driver);
    this.unityContainer.RegisterInstance<IJavaScriptInvoker>(this.driver);
    this.unityContainer.RegisterInstance<INavigationService>(this.driver);
    this.unityContainer.RegisterInstance<IElementFinder>(this.driver);
}

BaseTest

C#
[TestClass]
    public class BaseTest
    {
        private readonly MSTestExecutionProvider currentTestExecutionProvider;
        private IDriver driver;
        private readonly IUnityContainer container;
        private TestContext testContextInstance;

        public BaseTest()
        {
            this.container = new UnityContainer();
            this.container.RegisterInstance<IUnityContainer>(this.container);
            this.currentTestExecutionProvider = new MSTestExecutionProvider();
            this.InitializeTestExecutionBehaviorObservers(
                this.currentTestExecutionProvider, this.container);
            var memberInfo = MethodInfo.GetCurrentMethod();
            this.currentTestExecutionProvider.TestInstantiated(memberInfo);
        }

        public IDriver Driver
        {
            get
            {
                return driver;
            }
        }

        public IUnityContainer Container
        {
            get
            {
                return container;
            }
        }

        public TestContext TestContext
        {
            get
            {
                return testContextInstance;
            }
            set
            {
                testContextInstance = value;
            }
        }

        public string TestName
        {
            get
            {
                return this.TestContext.TestName;
            }
        }

        [TestInitialize]
        public void CoreTestInit()
        {
            var memberInfo = GetCurrentExecutionMethodInfo();
            this.currentTestExecutionProvider.PreTestInit(this.TestContext, memberInfo);
            this.driver = this.container.Resolve<IDriver>();
            this.TestInit();
            this.currentTestExecutionProvider.PostTestInit(this.TestContext, memberInfo);
        }

        [TestCleanup]
        public void CoreTestCleanup()
        {
            var memberInfo = GetCurrentExecutionMethodInfo();
            this.currentTestExecutionProvider.PreTestCleanup(this.TestContext, memberInfo);
            this.TestCleanup();
            this.currentTestExecutionProvider.PostTestCleanup(this.TestContext, memberInfo);
        }

        public virtual void TestInit()
        {
        }

        public virtual void TestCleanup()
        {
        }

        private MethodInfo GetCurrentExecutionMethodInfo()
        {
            var memberInfo = this.GetType().GetMethod(this.TestContext.TestName);
            return memberInfo;
        }

        private void InitializeTestExecutionBehaviorObservers(
            MSTestExecutionProvider currentTestExecutionProvider, 
            IUnityContainer container)
        {
            var executionEngine = new ExecutionEngineBehaviorObserver(container);
            executionEngine.Subscribe(currentTestExecutionProvider);
        }
    }

This will be the base class for all of our tests. In the InitializeTestExecutionBehaviourObservers method, we subscribe the execution engine behaviour to the main MSTestExecutionProvider. This way when the CoreTestInit method is called before our tests, the PreTestInit logic of the TestExecutionBehaviourObserver will be executed. After the run of all PreTestInit methods, we resolve the IDriver so that we can pass its instance to the main test class through the Driver property.

Configure Execution Engine in Tests

Test Example

C#
[TestClass,
ExecutionEngineAttribute(ExecutionEngineType.TestStudio, Browsers.Firefox)]
public class BingTests : BaseTest
{
    [TestMethod]
    public void SearchForAutomateThePlanet()
    {
        var bingMainPage = this.Container.Resolve<BingMainPage>();
        bingMainPage.Navigate();
        bingMainPage.Search("Automate The Planet");
        bingMainPage.AssertResultsCountIsAsExpected(264);
    }
}

There are two things that you need to do to use the new execution engine's configuration logic. First, your test class needs to derive from the previously introduced BaseTest class. Secondly, you need to setup the ExecutionEngineAttribute. You can pass to its constructor the desired values for the execution engine's type and browser.

Design & Architecture

The post Create Hybrid Test Framework – Testing Framework Driver Implementation appeared first on Automate The Planet.

All images are purchased from DepositPhotos.com and cannot be downloaded and used for free.
License Agreement

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)