Advanced Observer Design Pattern via IObservable and IObserver in Automated Testing

Advanced Observer Design Pattern via IObservable and IObserver in Automated Testing

The “Design Patterns in Automated Testing” series are all about the integration of the most practical design patterns in the automation testing. Last two articles from the sequence were dedicated to the Observer Design Pattern. The first one was about the classic implementation of the pattern and the second one about its events-delegates substitute. There is a third possible implementation of the observer design pattern that is a new one for .NET. It uses the IObserver and IObservable interfaces. This last article dedicated to the observer design pattern is going to look into this new implementation.

UML Class Diagram

classDiagram
    ITestExecutionProvider <|.. MSTestExecutionProvider
    MSTestExecutionProvider --> Unsubscriber
    MSTestExecutionProvider o-- BaseTestBehaviorObserver
    BaseTestBehaviorObserver <|-- OwnerTestBehaviorObserver
    BaseTest --> MSTestExecutionProvider
    class ITestExecutionProvider {
        <<interface>>
        +Subscribe(IObserver~ExecutionStatus~ observer)
    }
    class MSTestExecutionProvider {
        +Subscribe(IObserver~ExecutionStatus~ observer)
        +PreTestInit(ExecutionStatus status)
        +PostTestInit(ExecutionStatus status)
    }
    class ExecutionStatus {
        +TestContext Context
        +MemberInfo MemberInfo
    }
    class BaseTestBehaviorObserver {
        +Subscribe(IObservable~ExecutionStatus~ provider)
        +Unsubscribe()
        +OnNext(ExecutionStatus status)
    }
    class OwnerTestBehaviorObserver {
        +OnNext(ExecutionStatus status)
    }
    class Unsubscriber {
        +Dispose()
    }
    class BaseTest {
        -ITestExecutionProvider provider
    }

Participants

The classes and objects participating in this pattern are:

  • ITestExecutionProvider

    Contains primary method definitions that every execution provider should implement.

  • 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.

  • ExecutionStatus

    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 particular observer can be any class that implements the BaseObserver class. Each observer registers with a particular provider to receive updates via subscribing to the supplier’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.

  • Unsubscriber

    Object that is used to healthy dispose observers when they go out of scope.

Observer Design Pattern C# Code

Use Case

classDiagram
    IObservable~ExecutionStatus~ <|.. MSTestExecutionProvider
    IDisposable <|.. MSTestExecutionProvider
    IDisposable <|.. Unsubscriber~T~
    IObserver~ExecutionStatus~ <|.. BaseTestBehaviorObserver
    MSTestExecutionProvider --> Unsubscriber~T~
    BaseTestBehaviorObserver <|-- OwnerTestBehaviorObserver
    class MSTestExecutionProvider {
        -List~IObserver~ExecutionStatus~~ observers
        +Subscribe(IObserver~ExecutionStatus~ observer) IDisposable
        +PreTestInit(TestContext ctx, MemberInfo memberInfo)
        +Dispose()
    }
    class Unsubscriber~T~ {
        +Dispose()
    }
    class BaseTestBehaviorObserver {
        -IDisposable cancellation
        +Subscribe(IObservable~ExecutionStatus~ provider)
        +Unsubscribe()
        +OnNext(ExecutionStatus status)
        +OnError(Exception e)
        +OnCompleted()
    }
    class OwnerTestBehaviorObserver {
        +OnNext(ExecutionStatus status)
    }

One of the differences between the classic implementation and the IObservable one is that the TestExecutionProvider doesn’t hold Unsubscribe/Detach method. Next there is a big difference in the implementation of the Subscribe method. The new implementation uses a new class called Unsubscriber which task is to healthy release any Observer that goes out of scope.

public class MSTestExecutionProvider : IObservable<ExecutionStatus>, IDisposable, ITestExecutionProvider
{
    private readonly List<IObserver<ExecutionStatus>> testBehaviorObservers;

    public MSTestExecutionProvider()
    {
        this.testBehaviorObservers = new List<IObserver<ExecutionStatus>>();
    }

    public void PreTestInit(TestContext context, MemberInfo memberInfo)
    {
        this.NotifyObserversExecutionPhase(context, memberInfo, ExecutionPhases.PreTestInit);
    }

    public void PostTestInit(TestContext context, MemberInfo memberInfo)
    {
        this.NotifyObserversExecutionPhase(context, memberInfo, ExecutionPhases.PostTestInit);
    }

    public void PreTestCleanup(TestContext context, MemberInfo memberInfo)
    {
        this.NotifyObserversExecutionPhase(context, memberInfo, ExecutionPhases.PreTestCleanup);
    }

    public void PostTestCleanup(TestContext context, MemberInfo memberInfo)
    {
        this.NotifyObserversExecutionPhase(context, memberInfo, ExecutionPhases.PostTestCleanup);
    }

    public void TestInstantiated(MemberInfo memberInfo)
    {
        this.NotifyObserversExecutionPhase(null, memberInfo, ExecutionPhases.TestInstantiated);
    }

    public IDisposable Subscribe(IObserver<ExecutionStatus> observer)
    {
        if (!testBehaviorObservers.Contains(observer))
        {
            testBehaviorObservers.Add(observer);
        }
        return new Unsubscriber<ExecutionStatus>(testBehaviorObservers, observer);
    }

    private void NotifyObserversExecutionPhase(TestContext context, MemberInfo memberInfo, ExecutionPhases executionPhase)
    {
        foreach (var currentObserver in this.testBehaviorObservers)
        {
            currentObserver.OnNext(new ExecutionStatus(context, memberInfo, executionPhase));
        }
    }

    public void Dispose()
    {
        foreach (var currentObserver in this.testBehaviorObservers)
        {
            currentObserver.OnCompleted();
        }

        this.testBehaviorObservers.Clear();
    }
}

The Unsubscriber is a generic class that implements the IDisposable interface. It takes care of the removal process of the concrete observer from the provider’s collection. In the other implementations of the Observer Design Pattern, when an observer goes out of scope you might think that its memory will be cleaned by the Garbage Collector. However, if its reference held in the provider is not properly released, the GC will wait until the supplier goes out a scope to clean the observer’s memory. The implementation of the IObserver and IObservable interfaces prevents it.

internal class Unsubscriber<T> : IDisposable
{
    private List<IObserver<T>> observers;
    private IObserver<T> observer;

    internal Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer)
    {
        this.observers = observers;
        this.observer = observer;
    }

    public void Dispose()
    {
        if (observers.Contains(observer))
            observers.Remove(observer);
    }
}

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.

Notification Methods Exposed by IObserver Interface

public class ExecutionStatus
{
    public TestContext TestContext { get; set; }

    public MemberInfo MemberInfo { get; set; }

    public ExecutionPhases ExecutionPhase { get; set; }

    public ExecutionStatus(TestContext testContext, ExecutionPhases executionPhase) : this(testContext, null, executionPhase)
    {
    }

    public ExecutionStatus(TestContext testContext, MemberInfo memberInfo, ExecutionPhases executionPhase)
    {
        this.TestContext = testContext;
        this.MemberInfo = memberInfo;
        this.ExecutionPhase = executionPhase;
    }
}
public enum ExecutionPhases
{
    TestInstantiated,
    PreTestInit,
    PostTestInit,
    PreTestCleanup,
    PostTestCleanup
}

The provider calls the OnNext method of each subscribed observer in the specific notification points, passing a new instance of the ExecutionStatus data object.

private void NotifyObserversExecutionPhase(TestContext context, MemberInfo memberInfo, ExecutionPhases executionPhase)
{
    foreach (var currentObserver in this.testBehaviorObservers)
    {
        currentObserver.OnNext(new ExecutionStatus(context, memberInfo, executionPhase));
    }
}

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 Base Observer Using IObserver Interface

The base observer class implements the IObserver interface. It requires the implementation of the previously mentioned three methods- OnNext, OnError, and OnCompleted. However, the class holds more important methods like Subscribe and Unsubscribe. Also, exposes empty protected virtual methods to its childs for the different notification points like PostTestCleanup and PreTestInit.

public class BaseTestBehaviorObserver : IObserver<ExecutionStatus>
{
    private IDisposable cancellation;

    public virtual void Subscribe(IObservable<ExecutionStatus> provider)
    {
        cancellation = provider.Subscribe(this);
    }

    public virtual void Unsubscribe()
    {
        cancellation.Dispose();
    }

    public void OnNext(ExecutionStatus currentExecutionStatus)
    {
        switch (currentExecutionStatus.ExecutionPhase)
        {
            case ExecutionPhases.TestInstantiated:
                this.TestInstantiated(currentExecutionStatus.MemberInfo);
                break;
            case ExecutionPhases.PreTestInit:
                this.PreTestInit(currentExecutionStatus.TestContext, currentExecutionStatus.MemberInfo);
                break;
            case ExecutionPhases.PostTestInit:
                this.PostTestInit(currentExecutionStatus.TestContext, currentExecutionStatus.MemberInfo);
                break;
            case ExecutionPhases.PreTestCleanup:
                this.PreTestCleanup(currentExecutionStatus.TestContext, currentExecutionStatus.MemberInfo);
                break;
            case ExecutionPhases.PostTestCleanup:
                this.PostTestCleanup(currentExecutionStatus.TestContext, currentExecutionStatus.MemberInfo);
                break;
            default:
                break;
        }
    }

    public virtual void OnError(Exception e)
    {
        Console.WriteLine("The following exception occurred: {0}", e.Message);
    }

    public virtual void OnCompleted()
    {
    }

    protected virtual void PreTestInit(TestContext context, MemberInfo memberInfo)
    {
    }

    protected virtual void PostTestInit(TestContext context, MemberInfo memberInfo)
    {
    }

    protected virtual void PreTestCleanup(TestContext context, MemberInfo memberInfo)
    {
    }

    protected virtual void PostTestCleanup(TestContext context, MemberInfo memberInfo)
    {
    }

    protected virtual void TestInstantiated(MemberInfo memberInfo)
    {
    }
}
public class OwnerTestBehaviorObserver : BaseTestBehaviorObserver
{
    protected override void PreTestInit(TestContext context, MemberInfo memberInfo)
    {
        this.ThrowExceptionIfOwnerAttributeNotSet(memberInfo);
    }

    private void ThrowExceptionIfOwnerAttributeNotSet(MemberInfo memberInfo)
    {
        try
        {
            memberInfo.GetCustomAttribute<OwnerAttribute>(true);
        }
        catch
        {
            throw new Exception("You have to set Owner of your test before you run it");
        }
    }
}

The only difference compared to the other implementations of the Observer Design Pattern in the concrete observer is that the Pre and Post methods are marked as protected.

Assemble Everything 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. Although it is possible to attach an observer to multiple providers, the recommended pattern is to connect an IObserver instance to only one IObservable instance.


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 static void OnClassInitialize(TestContext context)
    {
    }

    
    public static void OnClassCleanup()
    {
    }

    
    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);
    }
}

Download full source code

Related Articles

Design Patterns

Full-Stack Test Automation Frameworks- Video Recording on Test Failure

Some of the must-have features for 5th generation frameworks are related to troubleshooting easiness. With the increasing tests count and complexity, it will be

Full-Stack Test Automation Frameworks- Video Recording on Test Failure

Design Patterns

Advanced Behaviours Design Pattern in Automated Testing Part 1

In my previous article dedicated to Behaviours Design Pattern, I shared with you how you can use the pattern to build system tests like a LEGO. The new article

Advanced Behaviours Design Pattern in Automated Testing Part 1

Design Patterns

Use IoC Container to Create Page Object Pattern on Steroids

In my previous articles from the series "Design Patterns in Automated Testing", I explained in details how to make your test automation framework better through

Use IoC Container to Create Page Object Pattern on Steroids

Design Patterns

Page Object Pattern in Automated Testing

In my new series of articles "Design Patterns in Automated Testing", I am going to present you the most useful techniques for structuring the code of your autom

Page Object Pattern in Automated Testing

Design Patterns

Simple Factory Design Pattern- WebDriver Anonymous Browsing with Rotating Proxies

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

Simple Factory Design Pattern- WebDriver Anonymous Browsing with Rotating Proxies

Design Patterns

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 B

Behaviours 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.