Introduction
The “Design Patterns in Automation 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
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. The 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 extend 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
The primary goal of the sample code is to provide an easy way to automation engineers to add additional logic to the current test execution via class/test level attributes. For example, configure current test execution browser or fail the test if the owner attribute is not set.
The following class structure is going to be used.
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
whose 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);
}
}
By the way during my research for the “Design Patterns in Automation Testing” series, I always first read about the presented pattern in several books. One of them that you might want to check is “Head First Design Patterns” by Eric Freeman. The author uses a very unique methodology for presenting the material that I haven’t found anywhere else. Probably most of you will like it. For the more hardcore fans that might find the book too easy, I recommend the bible of the design patterns - “Design Patterns- Elements of Reusable Object-Oriented Software”. It will change your way of thinking about object-oriented design.
Notification Methods Exposed by IObserver Interface
The another big difference in this implementation is that the observers interface IObserver
presents only three public
methods to the concrete provider- OnNext
, OnError
, and OnCompleted
.
OnNext
– The provider calls this method to send notifications to the subscribed observers. OnError
– This method notifies the observers that an error happened in the reported data. Not necessary means that an exception occurred. It should be seen as informational, the provider should not expect the observers to provide an error handling. OnCompleted
– Reports that no further data is available.
To be able to support multiple notification points with different behavior, we use the ExecutionPhases
enumeration to notify the particular observers for the current execution status. This enum
’s reference is held in the object that is used to pass notification data between the provider and its observers - ExecutionStatus
.
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;
}
}
It holds three properties- the MSTest TestContext
object, reflection MemberInfo
object containing an information about the currently executed test method and the current execution phase.
There are five possible execution phases:
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));
}
}
Create Base Observer Using IObserver Interface
The base observer class implements the IObserver<ExecutionStatus>
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, it exposes empty protected virtual
methods to its children 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)
{
}
}
The Subscribe
method accepts a provider instance as a parameter and calls its Subscribe
implementation to attach itself. The Unsubscribe
method only calls the Unsubscriber
’s Dispose
method where the observer is removed from the provider’s observers collection.
The base OnNext
method performs different actions based on the current execution phase. The default behavior is that if the child behaviors don’t override the empty implementations of the pre and post methods, nothing will happen. With this mechanism in hand, the concrete observers can implement only the needed notification point methods, not all of them.
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<T>
instance to only one IObservable<T>
instance.
[TestClass]
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;
}
}
[ClassInitialize]
public static void OnClassInitialize(TestContext context)
{
}
[ClassCleanup]
public static void OnClassCleanup()
{
}
[TestInitialize]
public void CoreTestInit()
{
var memberInfo = GetCurrentExecutionMethodInfo();
this.currentTestExecutionProvider.PreTestInit(this.TestContext, memberInfo);
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)
{
new AssociatedBugTestBehaviorObserver().Subscribe(currentTestExecutionProvider);
new BrowserLaunchTestBehaviorObserver().Subscribe(currentTestExecutionProvider);
new OwnerTestBehaviorObserver().Subscribe(currentTestExecutionProvider);
}
}
So Far in the "Design Patterns in Automated Testing" Series
- Page Object Pattern
- Advanced Page Object Pattern
- Facade Design Pattern
- Singleton Design Pattern
- Fluent Page Object Pattern
- IoC Container and Page Objects
- Strategy Design Pattern
- Advanced Strategy Design Pattern
- Observer Design Pattern
- Observer Design Pattern via Events and Delegates
- Observer Design Pattern via IObservable and IObserver
- Decorator Design Pattern- Mixing Strategies
- Page Objects That Make Code More Maintainable
- Improved Facade Design Pattern in Automation Testing v.2.0
- Rules Design Pattern
- Specification Design Pattern
- Advanced Specification Design Pattern
If you enjoy my publications, feel free to SUBSCRIBE
Also, hit these share buttons. Thank you!
Source Code
References
The post Advanced Observer Design Pattern via IObservable and IObserver in Automation Testing appeared first on Automate The Planet.
All images are purchased from DepositPhotos.com and cannot be downloaded and used for free. License Agreement