Enterprise Test Automation Framework: Inversion of Control Containers

Enterprise Test Automation Framework: Inversion of Control Containers

In the new Build Enterprise Automation Framework Series, we will look into detailed explanations on creating enterprise test automation frameworks. Many people starting a new position have this particular assignment, so I think it is crucial to understand how to follow all high-quality standards and practices properly. I am not a fan of reinventing the wheel, so I will encourage you to leverage other proven open-source solutions if they can be used and fit your needs instead of writing your own. Leverage the knowledge and expertise of proven experts in the field. If you follow this advice, this series will be highly useful to you again because you will understand how the test automation frameworks are designed, written, and maintained + how to work internally. The information about the subject will be invaluable to you choosing the right tool for your tests. The series can be even more useful to you if you decide to do everything from scratch.

Article by article, we will discuss different aspects and features of the enterprise test automation frameworks. At the end of the course, we will have a full-fledged enterprise test automation framework. The first article from the series will talk not so much about code but rather on how to define what we need (our requirements), how to do research properly- finding the right tools that we will base our framework, and lastly, creating a detailed list of desired characteristics for our solution grouping all of the various features that we will build in the next articles. In the course, I will use as a demo/example our open-source BELLATRIX test automation framework, which I believe is one of the most feature richest open-source frameworks out there.

In the first article from the series “Enterprise Test Automation Framework: Define Requirements and Characteristics”, we defined our future framework’s requirements and qualities/characteristics. This publication will continue with defining more precisely the various features grouped by the previously described characteristics. They will help us to prioritize and plan our work on the framework. We already looked into a few features in part 1part 2 and part3. Planning and designing framework modularity is essential for supporting more comfortable usage, maintainability, and extensibility. This is why we created such a plan in the Modularity Planning and Design publication. Now we will start designing and building the plug-in architecture of the framework. The first step will be to incorporate inversion of control containers, which will help us to enable modularity and support framework extensibility.

What Is Inversion of Control?

Definition

In software engineering, inversion of control is a programming principle. IoC inverts the flow of control as compared to traditional control flow. In IoC, custom-written portions of a computer program receive the flow of control from a generic framework. – Wikipedia

  • Inversion of control IoC

    Is a generic term meaning rather than having the application call the methods in a framework, the framework calls implementations provided by the application.

  • Dependency inversion DI

    Is a form of IoC, where implementations are passed into an object through constructors/setters/service locators, which the item will ‘depend’ on to behave correctly.

  • Dependency inversion Frameworks

    Are designed to make use of DI and can define interfaces (or Annotations in Java) to make it easy to pass in the implementations.

  • Inversion of control containers

    are DI frameworks that can work outside of the programming language. You can configure which implementations to use in metadata files (XML, JSON).

DIP – Dependency Inversion Principle

Definition

High-level classes should not depend on low-level classes. Both types should be created based on abstractions. These abstractions shouldn’t depend on any low-level details. All details should use abstractions instead.

The high-level classes are usually the ones that contain the business logic, and the low-level ones consist of actions such as CRUD DB operations, reading files or calling web APIs. When we say that a class is tightly coupled it means that it has access to low-level details (that it shouldn’t have) instead of using the abstractions.

How about extending our CustomerOrder example a bit? The requirements have already changed, and we have different types of orders to process- silver, gold and platinum. Also, there are new requirements which state that we need to add two more ways of notifications in case something happens. For example, sending an email and SMS with updates. To do so:

We will create a common interface for all loggers called ILogger.

public interface ILogger
{
    void CreateLogEntry(string errorMessage);
}

After that we will create three separate loggers- one for file logging, one for email and one more for SMS.

public class FileLogger: ILogger
{
    public void CreateLogEntry(string errorMessage)
    {
        File.WriteAllText(@"C:exceptions.txt", errorMessage);
    }
}

public class EmailLogger: ILogger {
    public void CreateLogEntry(string errorMessage)
    {
        EmailFactory.SendEmail(errorMessage);
    }
}

public class SmsLogger: ILogger
{
    public void CreateLogEntry(string errorMessage)
    }
        SmsFactory.SendSms(errorMessage);
    }
}

The business logic behind the new requirements is that we need to send an email for gold orders and an SMS for platinum orders since they bring our company more money.

public class CustomerOrder
{
    public void Create(OrderType orderType)
    {
        try {

            // Database code goes here
        }
        catch (Exception ex)
        {
            switch (orderType)
            {
            case OrderType.Platinum:
                new SmsLogger().CreateLogEntry(ex.Message);
                break;
            case OrderType.Gold:
                new EmailLogger().CreateLogEntry(ex.Message);
                break;
            default:
                new FileLogger().CreateLogEntry(ex.Message);
                break;
            }
        }
    }
}

The code violates the Single Responsibility principle again. It should be focused on creating purchases, but also decides which object to be created, while it is not the work of the CustomerOrder class to determine which instances of the ILogger should be used. The biggest problem here is related to the new keyword. This is an extra responsibility of making the decision which objects to be created, so if we delegate this responsibility to someone other than the CustomerOrder class, that will solve the problem.

We can have different child classes of CustomerOrder for the different types of orders. Also, the logger can be passed as dependency rather than creating it in the method itself.

public class CustomerOrder
{
    private ILogger _logger;
    public CustomerOrder(ILogger logger)
    {
        _logger = logger;
    }

    public void Create()
    {
        try
        {
            // Database code goes here
        }
        catch (Exception ex)
        {
            _logger.CreateLogEntry(ex.Message);
        }
    }
}

public class GoldCustomerOrder : CustomerOrder
{
    public GoldCustomerOrder() : base(new EmailLogger()) {}
}

public class PlatinumCustomerOrder : CustomerOrder
{
    public PlatinumCustomerOrder() : base(new SmsLogger()) {}
}

What Is Inversion of Control Container?

The inversion of control container creates an object of the specified class and injects all the dependency objects through a constructor, a property, or a method at run time and disposes it appropriately. This is done so that we don’t have to create and manage objects manually.

Most IoC containers support the following dependency injection life-cycle.

  • Register

    the container must know which dependency to instantiate when it encounters a particular type. This process is called registration. It must include some way to register type-mapping.

  • Resolve

    when using the IoC container, we don’t need to create objects manually. The container does it for us. This is called resolution. The container must include some methods to resolve the specified type; the container creates an object of the specified type, injects the required dependencies if any, and returns the object.

  • Dispose

    the container must manage the lifetime of the dependent objects. Most IoC containers include different lifetime managers to control an object’s life-cycle and dispose of it.

Framework IoC Interfaces

We will design our framework so that the framework will use the IoC container via interfaces, which will allow us to have different implementations of it. If we want to change it at some point, it will be relatively easy. We will have to implement the same interfaces and point that we want to use them. We will put all of the logic in a project called Bellatrix.ServicesCollection

We will have three interfaces. IServicesResolvingCollection containing methods for retrieving objects from the container, IServicesRegisteringCollection holding logic for configuring the classes that will be later initialized and retrieved, and a third one IServicesCollection combining both plus adding functions for working with child containers enabling us to run the tests in parallel.

public interface IServicesResolvingCollection {
    T Resolve<T>(bool shouldThrowResolveException = false);
    T Resolve<T>(string name, bool shouldThrowResolveException = false);
    object Resolve(Type type, bool shouldThrowResolveException = false);
    T Resolve<T>(bool shouldThrowResolveException = false,
        params OverrideParameter[] overrides);
    IEnumerable<T> ResolveAll<T>(bool shouldThrowResolveException = false);
    IEnumerable<T> ResolveAll<T> (bool shouldThrowResolveException = false,
        params OverrideParameter[] overrides);
}

We have generic methods where we need to provide the type we want to retrieve from the container. It has to be first registered to the container.

The interface for registration logic is slightly larger because there are lots of variations of the functions. Again, there are many methods accepting generics where we map an interface/contract (TFrom) to the actual implementation (TTo) that will later be initialized from the container. If some type needs additional more complex initialization, we can use an injection constructor object to supply the custom build logic. Also, we have simple control over the life-cycle with the argument - shouldUseSingleton instructing the container to keep a single instance for the particular object instead of creating a new one each time. Or you can directly use the RegisterInstance methods for the same purpose.

public interface IServicesRegisteringCollection
{
    bool IsRegistered<TInstance>();
    void RegisterType<TFrom>();
    void RegisterSingleInstance <TFrom,TTo>(InjectionConstructor injectionConstructor)
                                where TTo : TFrom;
    void RegisterSingleInstance<TFrom>(Type instanceType, InjectionConstructor injectionConstructor);
    void UnregisterSingleInstance<TFrom>();
    void UnregisterSingleInstance<TFrom>(string name);
    void RegisterType<TFrom,TTo>(string name, InjectionConstructor injectionConstructor)
                     where TTo : TFrom;
    void RegisterNull<TFrom>();
    void RegisterType<TFrom>(bool shouldUseSingleton);
    void RegisterType<TFrom,TTo>()
                    where TTo : TFrom;
    void RegisterType<TFrom,TTo>(string name)
                    where TTo : TFrom;
    void RegisterType<TFrom,TTo>(bool shouldUseSingleton)
                    where TTo : TFrom;
    void RegisterInstance<TFrom>(TFrom instance, bool shouldUseSingleton = false);
    void RegisterInstance<TFrom>(TFrom instance, string name);
    object CreateInjectionParameter<TInstance>();
    object CreateValueParameter(object value);
}

Here is the main interface, which derives from IServicesRegisteringCollection and IServicesResolvingCollection. It adds four methods for working with child containers.

public interface IServicesCollection : IServicesRegisteringCollection,
                 IServicesResolvingCollection, IDisposable
{
    List <IServicesCollection> GetChildServicesCollections();
    IServicesCollection CreateChildServicesCollection(string collectionName);
    IServicesCollection FindCollection(string collectionName);
    bool IsPresentServicesCollection(string collectionName);
}

Unity IoC Parallel Execution Implementation

For main implementation of the above interfaces we will Unity Inversion of Control Container.

NOTE: The Unity Container (Unity) is a full featured, extensible dependency injection container. It facilitates building loosely coupled applications. You can use it installing Unity NuGet package to your project.

For making the example shorter, I left only some of the essential methods because others are implemented in a similar fashion.

public class UnityServicesCollection : IServicesCollection
{
    private readonly IUnityContainer _container;
    private readonly Dictionary<string,IServicesCollection> _containers;
    private readonly object _lockObject = new object();
    private bool _isDisposed;

    public UnityServicesCollection()
    {
        _containers = new Dictionary<string,IServicesCollection>();
    }

    public UnityServicesCollection(IUnityContainer container)
    {
        _container = container;
        _containers = new Dictionary<string,IServicesCollection>();
    }

    public T Resolve<T>(bool shouldThrowResolveException = false)
    {
        T result = default;
        try
        {
            lock(_lockObject)
            {
                result = _container.Resolve<T>();
            }
        }
        catch (Exception ex)
        {
            if (shouldThrowResolveException)
            {
                throw;
            }
        }

        return result;
    }

    public IEnumerable<T> ResolveAll<T>(bool shouldThrowResolveException = false)
    {
        IEnumerable <T> result;
        try
        {
            lock(_lockObject)
            {
                result = _container.ResolveAll<T>();
            }
        }
        catch (Exception ex)
        {
            if (ex.InnerException != null && shouldThrowResolveException)
            {
                throw ex.InnerException;
            }

            throw;
        }

        return result;
    }

    public void RegisterType<TFrom,TTo>()
                            where TTo: TFrom
    {
        lock(_lockObject)
        {
            _container.RegisterType<TFrom,TTo>();
        }
    }

    public void RegisterType<TFrom,TTo>(bool useSingleInstance)
                             where TTo: TFrom
    {
        lock(_lockObject)
        {
            _container.RegisterType<TFrom,TTo>(
                new ContainerControlledLifetimeManager()
            );
        }
    }

    public void RegisterInstance<TFrom>(TFrom instance, bool useSingleInstance = false)
    {
        if (useSingleInstance)
        {
            lock(_lockObject)
            {
                _container.RegisterInstance(instance,
                new ContainerControlledLifetimeManager()
                );
            }
        }
        else
        {
            lock(_lockObject)
            {
                _container.RegisterInstance(instance);
            }
        }
    }

    public IServicesCollection CreateChildServicesCollection(string collectionName)
    {
        lock(_lockObject)
        {
            var childNativeContainer = _container.CreateChildContainer();
            var childServicesCollection =
                    new UnityServicesCollection(childNativeContainer);
            if (_containers.ContainsKey(collectionName))
            {
                _containers = childServicesCollection;
            }
            else
            {
                _containers.Add(collectionName, childServicesCollection);
            }

            return childServicesCollection;
        }
    }

    public void Dispose()
    {
        if (!_isDisposed)
        {
            _container.Dispose();
            GC.SuppressFinalize(this);
            _isDisposed = true;
        }
    }

    public IServicesCollection FindCollection(string collectionName)
    {
        lock(_lockObject)
        {
            if (_containers.ContainsKey(collectionName))
            {
                return _containers;
            }

            return this;
        }
    }

    public bool IsPresentServicesCollection(string collectionName)
    {
        lock(_lockObject)
        {
            if (_containers.ContainsKey(collectionName))
            {
                return true;
            }

            return false;
        }
    }

    // the rest of the methods
}

To enable parallel usage of the container, we use the lock statement and our custom object _lockObject to ensure no deadlocks occur. Please note that everywhere we use the IUnityContainer instead of the actual class, which we set in the constructor. We will create an instance in a separate class when we talk about the Service Locator design pattern. The next interesting part is the usage of the dictionary collection _containers, which will hold a separate container for each test class to enable parallel test execution. To register a type as a singleton, we pass an instance of ContainerControlledLifetimeManager to the RegisterType method of the Unity container. To add the support for child containers, we leverage entirely to the native Unity support for them.

Service Locator Design Pattern

Definition

The service locator pattern is a design pattern that is used to decouple a class from its dependencies. Instead of instantiating its dependencies directly, the dependent class requests them from a centralized service locator object.

Participants

  • RegistrationExtensions

    This class is responsible for initializing the service objects and storing them in the service locator.

  • IService

    These interfaces define the available members of the service classes that implement them. The framework classes use them.

  • Service

    The actual implementation of the IService interfaces. We don’t use these classes directly in the framework. We link the service interfaces and them in the RegistrationExtensions classes, and after that, we work in the framework classes with the interfaces instead of the actual implementations. We can later switch to other technologies relatively easy just replacing the registration through the service locator.

  • ServiceLocator

    the class implements the Singleton design pattern, ensuring that only a single instance of the service locator is available during the test run. It is responsible for searching and retrieving the initialized objects from the hidden IoC container.

  • FrameworkProject

    any class within the framework that should use any of the registered types. Instead of initializing them explicitly, it uses the ServiceLocator class, which is responsible for getting them from the IoC container.

UML Class Diagram

classDiagram
    IService <|.. Service
    RegistrationExtensions --> IService
    RegistrationExtensions --> ServiceLocator
    FrameworkProject --> ServiceLocator
    ServiceLocator --> IService
    class IService {
        <<interface>>
    }
    class Service {
    }
    class RegistrationExtensions {
        +RegisterServices()
    }
    class ServiceLocator {
        -static ServiceLocator instance
        +static Instance ServiceLocator
        +Get~T~() T
        +RegisterInstance~T~(T instance)
    }
    class FrameworkProject {
    }

Service Locator Design Pattern Implementation

public sealed class ServicesCollection
{
    private static IServicesCollection _serviceProvider;

    public static IServicesCollection Main
    {
        get
        {
            if (_serviceProvider == null)
            {
                var unityContainer = new UnityContainer();
                _serviceProvider = new
                    UnityServicesCollection(unityContainer);
                _serviceProvider.RegisterInstance(unityContainer);
            }

            return _serviceProvider;
        }
    }
}

The implementation of the pattern is quite simple. We have a private static field to the main IoC interface, and we have a public static property called Main, which has only a get method. Inside the get the first time we interact with the property, we initialize the inner IoC container. Afterward, we will work with its static instance. Since the property is static, we don’t need any variables to hold the locator’s instance. Instead, we can use it directly everywhere. It is especially useful for working with static constructors and extensions methods. You can read more about the Singleton design pattern in the article - Page Objects- Partial Classes Singleton Design Pattern- WebDriver C#

Below you can find a sample usage of the service locator in static method responsible for disposing WinAppDriver.

public static void Dispose(IServicesCollection childContainer)
{
    var webDriver = childContainer.
        Resolve<WindowsDriver<WindowsElement>>();
    webDriver?.Quit();
    webDriver?.Dispose();
    childContainer
        .UnregisterSingleInstance<WindowsDriver<WindowsElement>>();
    webDriver = ServicesCollection.Main
        .Resolve<WindowsDriver<WindowsElement>>();
    webDriver?.Quit();
    webDriver?.Dispose();
    ServicesCollection.Main
        .UnregisterSingleInstance<WindowsDriver<WindowsElement>>();
}

Summary

In the article, we looked at how to incorporate inversion of control containers, enabling modularity, and support framework extensibility. Before building the framework’s plug-in module, we will need first to create a module for storing and retrieving configuration data from files. We will discuss how to do that in the next article from the series.

Related Articles

Enterprise Test Automation Framework

Enterprise Test Automation Framework: Define Features Part 3

In the new Build Enterprise Automation Framework Series, we will look into detailed explanations on creating enterprise test automation frameworks. Many people

Enterprise Test Automation Framework: Define Features Part 3

Enterprise Test Automation Framework

Enterprise Test Automation Framework: Define Features Part 2

In the new Build Enterprise Automation Framework Series, we will look into detailed explanations on creating enterprise test automation frameworks. Many people

Enterprise Test Automation Framework: Define Features Part 2

Enterprise Test Automation Framework

Enterprise Test Automation Framework: Logging Module Design

In the new Build Enterprise Automation Framework Series, we will look into detailed explanations on creating enterprise test automation frameworks. Many people

Enterprise Test Automation Framework: Logging Module Design

Enterprise Test Automation Framework

Enterprise Test Automation Framework: Define Features Part 1

In the new Build Enterprise Automation Framework Series, we will look into detailed explanations on creating еnterprise test automation frameworks. Many people

Enterprise Test Automation Framework: Define Features Part 1

Enterprise Test Automation Framework

Enterprise Test Automation Framework: Plugin Architecture in MSTest

In the new Build Enterprise Automation Framework Series, we will look into detailed explanations on creating enterprise test automation frameworks. Many people

Enterprise Test Automation Framework: Plugin Architecture in MSTest

Enterprise Test Automation Framework

Enterprise Test Automation Framework: Define Requirements and Characteristics

In the new Build Enterprise Automation Framework Series, we will look into detailed explanations on creating enterprise test automation frameworks. Many people

Enterprise Test Automation Framework: Define Requirements and Characteristics
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.