Advanced Observer Design Pattern via Events and Delegates in Automated Testing

Advanced Observer Design Pattern via Events and Delegates in Automated Testing

In my articles from the series “Design Patterns in Automated Testing”, I am sharing with you ideas how to integrate the most useful code design patterns in the automation testing. In my last publication, I explained to you how to create an extendable test execution engine for Web Driver utilizing the classical implementation of the Observer Design Pattern. Here I am going to show you another more advanced implementation of the Observer Design Pattern using the power of the .NET’s events and delegates.

UML Class Diagram

classDiagram
    ITestExecutionProvider <|.. MSTestExecutionProvider
    BaseTestBehaviorObserver <|-- OwnerTestBehaviorObserver
    BaseTestBehaviorObserver --> MSTestExecutionProvider
    BaseTest --> MSTestExecutionProvider
    class ITestExecutionProvider {
        <<interface>>
        +event TestInstantiatedEvent
        +event PreTestInitEvent
        +event PostTestInitEvent
    }
    class MSTestExecutionProvider {
        +event TestInstantiatedEvent
        +event PreTestInitEvent
        +event PostTestInitEvent
    }
    class TestExecutionEventsArgs {
        +TestContext Context
        +MemberInfo MemberInfo
    }
    class BaseTestBehaviorObserver {
        +Subscribe(ITestExecutionProvider provider)
        +Unsubscribe()
        +PreTestInit(object sender, TestExecutionEventsArgs e)
    }
    class OwnerTestBehaviorObserver {
        +PreTestInit(object sender, TestExecutionEventsArgs e)
    }
    class BaseTest {
        -ITestExecutionProvider provider
    }

Participants

The classes and objects participating in this pattern are:

  • ITestExecutionProvider

    Objects use this interface to register as observers and also to remove themselves from being observers.

  • MSTestExecutionProvider

    The concrete provider/subject always implements the IProvider interface. Тhe particular provider holds different notification methods that are used to update all of the subscribed observers whenever the state changes.

  • TestExecutionEventsArgs

    The object that is used to transfer the state of the provider to the concrete observers.

  • BaseTestBehaviorObserver

    All potential observers need to inherit the base observer class. Additionally to the Subscribe and Unsubscribe methods, it holds empty methods that can be later overridden by the concrete observers.

  • OwnerTestBehaviorObserver

    A concrete observer can be any class that implements the BaseObserver class. Each observer registers with a particular provider to receive updates via subscribing to the provider’s events.

  • BaseTest

    The parent class for all test classes in the framework. Uses the TestExecutionProvider to extends its test execution capabilities via test/class level defined attributes and concrete observers.

  • BaseTest

    The parent class for all test classes in the framework. Uses the TestExecutionProvider to extends its test execution capabilities via test/class level defined attributes and concrete observers.

Observer Design Pattern C# Code

Use Case

classDiagram
    IExecutionProvider <|.. MSTestExecutionProvider
    BaseTestBehaviorObserver <|-- OwnerTestBehaviorObserver
    BaseTestBehaviorObserver --> MSTestExecutionProvider : subscribes
    class IExecutionProvider {
        <<interface>>
        +event TestInstantiatedEvent
        +event PreTestInitEvent
        +event PostTestInitEvent
        +event PreTestCleanupEvent
        +event PostTestCleanupEvent
    }
    class MSTestExecutionProvider {
        +event TestInstantiatedEvent
        +event PreTestInitEvent
        +event PostTestInitEvent
    }
    class BaseTestBehaviorObserver {
        -IDisposable cancellation
        +Subscribe(IExecutionProvider provider)
        +Unsubscribe()
    }
    class OwnerTestBehaviorObserver {
        +PreTestInit(object sender, TestExecutionEventArgs e)
    }
public interface IExecutionProvider
{
    event EventHandler<TestExecutionEventArgs> TestInstantiatedEvent;

    event EventHandler<TestExecutionEventArgs> PreTestInitEvent;

    event EventHandler<TestExecutionEventArgs> PostTestInitEvent;

    event EventHandler<TestExecutionEventArgs> PreTestCleanupEvent;

    event EventHandler<TestExecutionEventArgs> PostTestCleanupEvent;
}

The concrete provider looks almost identical to the previously developed with minor changes.

public class MSTestExecutionProvider : IExecutionProvider
{
    public event EventHandler<TestExecutionEventArgs> TestInstantiatedEvent;

    public event EventHandler<TestExecutionEventArgs> PreTestInitEvent;

    public event EventHandler<TestExecutionEventArgs> PostTestInitEvent;

    public event EventHandler<TestExecutionEventArgs> PreTestCleanupEvent;

    public event EventHandler<TestExecutionEventArgs> PostTestCleanupEvent;

    public void PreTestInit(TestContext context, MemberInfo memberInfo)
    {
        this.RaiseTestEvent(this.PreTestInitEvent, context, memberInfo);
    }

    public void PostTestInit(TestContext context, MemberInfo memberInfo)
    {
        this.RaiseTestEvent(this.PostTestInitEvent, context, memberInfo);
    }

    public void PreTestCleanup(TestContext context, MemberInfo memberInfo)
    {
        this.RaiseTestEvent(this.PreTestCleanupEvent, context, memberInfo);
    }

    public void PostTestCleanup(TestContext context, MemberInfo memberInfo)
    {
        this.RaiseTestEvent(this.PostTestCleanupEvent, context, memberInfo);
    }

    public void TestInstantiated(MemberInfo memberInfo)
    {
        this.RaiseTestEvent(this.TestInstantiatedEvent, null, memberInfo);
    }

    private void RaiseTestEvent(EventHandler<TestExecutionEventArgs> eventHandler, TestContext testContext, MemberInfo memberInfo)
    {
        if (eventHandler != null)
        {
            eventHandler(this, new TestExecutionEventArgs(testContext, memberInfo));
        }
    }
}

In the different test execution points, the method RaiseTestEvent is used to notify all subscribed observers for that particular execution point. If there are not any subscribers, the event is not triggered. The concrete observer’s needed information is passed by the creation of a new object of type TestExecutionEventArgs.

public class TestExecutionEventArgs : EventArgs
{
    private readonly TestContext testContext;
    private readonly MemberInfo memberInfo;

    public TestExecutionEventArgs(TestContext context, MemberInfo memberInfo)
    {
        this.testContext = context;
        this.memberInfo = memberInfo;
    }

    public MemberInfo MemberInfo
    {
        get
        {
            return this.memberInfo;
        }
    }

    public TestContext TestContext
    {
        get
        {
            return this.testContext;
        }
    }
}

It only contains two properties. The MSTest TestContext and the MemberInfo which is the reflection information about the currently executing test method.

While ago when we were working on the first version of the BELLATRIX test automation framework, I did this research and afterward we used a similar approach in many of the features of the solution.

Create Base Observer Using .NET Event and Delegates

As I have already pointed in the Events based implementation of the Observer Design Pattern, the base observer class doesn’t need to implement any interfaces.

public class BaseTestBehaviorObserver
{
    public void Subscribe(IExecutionProvider provider)
    {
        provider.TestInstantiatedEvent += this.TestInstantiated;
        provider.PreTestInitEvent += this.PreTestInit;
        provider.PostTestInitEvent += this.PostTestInit;
        provider.PreTestCleanupEvent += this.PreTestCleanup;
        provider.PostTestCleanupEvent += this.PostTestCleanup;
    }

    public void Unsubscribe(IExecutionProvider provider)
    {
        provider.TestInstantiatedEvent -= this.TestInstantiated;
        provider.PreTestInitEvent -= this.PreTestInit;
        provider.PostTestInitEvent -= this.PostTestInit;
        provider.PreTestCleanupEvent -= this.PreTestCleanup;
        provider.PostTestCleanupEvent -= this.PostTestCleanup;
    }

    protected virtual void TestInstantiated(object sender, TestExecutionEventArgs e)
    {
    }

    protected virtual void PreTestInit(object sender, TestExecutionEventArgs e)
    {
    }

    protected virtual void PostTestInit(object sender, TestExecutionEventArgs e)
    {
    }

    protected virtual void PreTestCleanup(object sender, TestExecutionEventArgs e)
    {
    }

    protected virtual void PostTestCleanup(object sender, TestExecutionEventArgs e)
    {
    }
}

In the Subscribe method, the concrete observer is subscribed to all available provider’s events. However, the wired methods are empty. This gives the specific child observer the flexibility to override only the needed methods. These parent methods are marked as protected so they cannot be put in an interface.

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

Create Concrete Observers Powered by Attributes

The primary goal was to create a way so that the user to be able to control the current test’s execution browser type through attributes. Below you can see that the usage of the BaseTest class and the ExecutionBrowser attribute didn’t change.


[ExecutionBrowser(BrowserTypes.Chrome)]
public class SearchEngineTestsDotNetEvents : BaseTest
{
    
    [ExecutionBrowser(BrowserTypes.Firefox)]
    public void SearchTextInSearchEngine_First_Observer()
    {
        B.SearchEngineMainPage searchEngineMainPage = new B.SearchEngineMainPage(Driver.Browser);
        searchEngineMainPage.Navigate();
        searchEngineMainPage.Search("Automate The Planet");
        searchEngineMainPage.ValidateResultsCount("RESULTS");
    }
}

The test execution flow stays intact. The only change in the concrete observers is that the overridden method should be marked as protected instead of as public.

public class BrowserLaunchTestBehaviorObserver : BaseTestBehaviorObserver
{
    protected override void PreTestInit(object sender, TestExecutionEventArgs e)
    {
        var browserType = this.GetExecutionBrowser(e.MemberInfo);
        Driver.StartBrowser(browserType);
    }

    protected override void PostTestCleanup(object sender, TestExecutionEventArgs e)
    {
        Driver.StopBrowser();
    }

    private BrowserTypes GetExecutionBrowser(MemberInfo memberInfo)
    {
        BrowserTypes result = BrowserTypes.Firefox;
        BrowserTypes classBrowserType = this.GetExecutionBrowserClassLevel(memberInfo.DeclaringType);
        BrowserTypes methodBrowserType = this.GetExecutionBrowserMethodLevel(memberInfo);
        if (methodBrowserType != BrowserTypes.NotSet)
        {
            result = methodBrowserType;
        }
        else if (classBrowserType != BrowserTypes.NotSet)
        {
            result = classBrowserType;
        }
        return result;
    }

    private BrowserTypes GetExecutionBrowserMethodLevel(MemberInfo memberInfo)
    {
        var executionBrowserAttribute = memberInfo.GetCustomAttribute<ExecutionBrowserAttribute>(true);
        if (executionBrowserAttribute != null)
        {
            return executionBrowserAttribute.BrowserType;
        }
        return BrowserTypes.NotSet;
    }

    private BrowserTypes GetExecutionBrowserClassLevel(Type type)
    {
        var executionBrowserAttribute = type.GetCustomAttribute<ExecutionBrowserAttribute>(true);
        if (executionBrowserAttribute != null)
        {
            return executionBrowserAttribute.BrowserType;
        }
        return BrowserTypes.NotSet;
    }
}

The code for controlling the browser type is almost identical with only the previously mentioned difference.

Putting All Together in BaseTest Class

Through the usage of separate classes for the implementation of the pattern, there are almost no changes in the BaseTest class. Only the implementations of the concrete provider and observers are replaced.

public class BaseTest
{
    private readonly MSTestExecutionProvider currentTestExecutionProvider;
    private TestContext testContextInstance;

    public BaseTest()
    {
        this.currentTestExecutionProvider = new MSTestExecutionProvider();
        this.InitializeTestExecutionBehaviorObservers(this.currentTestExecutionProvider);
        var memberInfo = MethodInfo.GetCurrentMethod();
        this.currentTestExecutionProvider.TestInstantiated(memberInfo);
    }

    public string BaseUrl { get; set; }

    public IWebDriver Browser { get; set; }

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

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

    
    public void CoreTestInit()
    {
        var memberInfo = GetCurrentExecutionMethodInfo();
        this.currentTestExecutionProvider.PreTestInit(this.TestContext, memberInfo);
        this.TestInit();
        this.currentTestExecutionProvider.PostTestInit(this.TestContext, memberInfo);
    }

    
    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)
    {
        new AssociatedBugTestBehaviorObserver().Subscribe(currentTestExecutionProvider);
        new BrowserLaunchTestBehaviorObserver().Subscribe(currentTestExecutionProvider);
        new OwnerTestBehaviorObserver().Subscribe(currentTestExecutionProvider);
    }
}

Related Articles

Design Patterns

Simple Factory Design Pattern- WebDriver Anonymous Browsing with Reverse Proxy

In the series “Design Patterns in Automated Testing“, you can read about the most useful techniques for structuring the automation tests' code. The article was

Simple Factory Design Pattern- WebDriver Anonymous Browsing with Reverse Proxy

Design Architecture, Design Patterns

Failed Tests Аnalysis – Decorator Design Pattern

Here I will present to you the third version of the Failed Tests Analysis engine part of the Design Patterns in Automated Testing Series. We are going to utilis

Failed Tests Аnalysis – Decorator Design Pattern

Design Patterns

Lazy Loading 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

Lazy Loading Design Pattern in Automated Testing

Design Patterns

Page Objects- Partial Classes Singleton Design Pattern- 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 Singleton Design Pattern- WebDriver C#

Design Patterns

Enhanced Selenium WebDriver Page Objects through Partial Classes

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

Enhanced Selenium WebDriver Page Objects through Partial Classes

Design Patterns

Composite 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

Composite Design Pattern 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.