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