Definition
The Observer Design Pattern defines a one-to-many dependency between objects so that when one object changes state, all of its dependents are notified and updated automatically.
Benefits
- Strive for loosley coupled designs between objects that interact.
- Allows you to send data to many other objects in a very efficient manner.
- No modification is need to be done to the subject to add new observers.
- You can add and remove observers at any time.
- The order of Observer notifications is undependable.
UML Class Diagram
classDiagram
ITestExecutionSubject <|.. MSTestExecutionSubject
ITestBehaviorObserver <|.. OwnerTestBehaviorObserver
MSTestExecutionSubject o-- ITestBehaviorObserver
BaseTest --> MSTestExecutionSubject
class ITestExecutionSubject {
<<interface>>
+Attach(ITestBehaviorObserver observer)
+Detach(ITestBehaviorObserver observer)
+PreTestInit(TestContext ctx, MemberInfo memberInfo)
+PostTestInit(TestContext ctx, MemberInfo memberInfo)
}
class MSTestExecutionSubject {
+Attach(ITestBehaviorObserver observer)
+Detach(ITestBehaviorObserver observer)
+PreTestInit(TestContext ctx, MemberInfo memberInfo)
+PostTestInit(TestContext ctx, MemberInfo memberInfo)
}
class ITestBehaviorObserver {
<<interface>>
+PreTestInit(TestContext ctx, MemberInfo memberInfo)
+PostTestInit(TestContext ctx, MemberInfo memberInfo)
}
class OwnerTestBehaviorObserver {
+PreTestInit(TestContext ctx, MemberInfo memberInfo)
+PostTestInit(TestContext ctx, MemberInfo memberInfo)
}
class BaseTest {
-ITestExecutionSubject subject
}
Participants
The classes and objects participating in this pattern are:
-
ITestExecutionSubject
Objects use this interface to register as observers and also to remove themselves from being observers.
-
MSTestExecutionSubject
The concrete subject always implements the ISubject interface. In addition to the attach and detach methods, the specific subject implements different notification methods that are used to update all of the subscribed observers whenever the state changes.
-
ITestBehaviorObserver
All potential observers need to implement the observer interface. These methods are called at the different points when the subject’s state changes.
-
OwnerTestBehaviorObserver
A concrete observer can be any class that implements IObserver interface. Each observer registers with a specific subject to receiving updates.
-
BaseTest
The parent class for all test classes in the framework. Uses the TestExecutionSubject to extends its test execution capabilities via test/class level defined attributes and concrete observers.
Observer Design Pattern C# Code
Use Case

public interface ITestExecutionSubject
{
void Attach(ITestBehaviorObserver observer);
void Detach(ITestBehaviorObserver observer);
void PreTestInit(TestContext context, MemberInfo memberInfo);
void PostTestInit(TestContext context, MemberInfo memberInfo);
void PreTestCleanup(TestContext context, MemberInfo memberInfo);
void PostTestCleanup(TestContext context, MemberInfo memberInfo);
void TestInstantiated(MemberInfo memberInfo);
}
public interface ITestBehaviorObserver
{
void PreTestInit(TestContext context, MemberInfo memberInfo);
void PostTestInit(TestContext context, MemberInfo memberInfo);
void PreTestCleanup(TestContext context, MemberInfo memberInfo);
void PostTestCleanup(TestContext context, MemberInfo memberInfo);
void TestInstantiated(MemberInfo memberInfo);
}
After that, a concrete subject class should be created.
public class MSTestExecutionSubject : ITestExecutionSubject
{
private readonly List<ITestBehaviorObserver> testBehaviorObservers;
public MSTestExecutionSubject()
{
this.testBehaviorObservers = new List<ITestBehaviorObserver>();
}
public void Attach(ITestBehaviorObserver observer)
{
testBehaviorObservers.Add(observer);
}
public void Detach(ITestBehaviorObserver observer)
{
testBehaviorObservers.Remove(observer);
}
public void PreTestInit(TestContext context, MemberInfo memberInfo)
{
foreach (var currentObserver in this.testBehaviorObservers)
{
currentObserver.PreTestInit(context, memberInfo);
}
}
public void PostTestInit(TestContext context, MemberInfo memberInfo)
{
foreach (var currentObserver in this.testBehaviorObservers)
{
currentObserver.PostTestInit(context, memberInfo);
}
}
public void PreTestCleanup(TestContext context, MemberInfo memberInfo)
{
foreach (var currentObserver in this.testBehaviorObservers)
{
currentObserver.PreTestCleanup(context, memberInfo);
}
}
public void PostTestCleanup(TestContext context, MemberInfo memberInfo)
{
foreach (var currentObserver in this.testBehaviorObservers)
{
currentObserver.PostTestCleanup(context, memberInfo);
}
}
public void TestInstantiated(MemberInfo memberInfo)
{
foreach (var currentObserver in this.testBehaviorObservers)
{
currentObserver.TestInstantiated(memberInfo);
}
}
}
public class BaseTestBehaviorObserver : ITestBehaviorObserver
{
private readonly ITestExecutionSubject testExecutionSubject;
public BaseTestBehaviorObserver(ITestExecutionSubject testExecutionSubject)
{
this.testExecutionSubject = testExecutionSubject;
testExecutionSubject.Attach(this);
}
public virtual void PreTestInit(TestContext context, MemberInfo memberInfo)
{
}
public virtual void PostTestInit(TestContext context, MemberInfo memberInfo)
{
}
public virtual void PreTestCleanup(TestContext context, MemberInfo memberInfo)
{
}
public virtual void PostTestCleanup(TestContext context, MemberInfo memberInfo)
{
}
public virtual void TestInstantiated(MemberInfo memberInfo)
{
}
}
As all notification methods are empty, the child class needs only to override the necessary ones. Also, the base class constructor requires a ITestExecutionSubject parameter in order to be able to associate the current observer to the subject.
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
Configure Test Execution Browser with Attribute
Now it is time to utilize all these classes to solve some practical problems. The primary goal is to create a way so that the user to be able to control the current test’s execution browser type through attributes.
[ExecutionBrowser(BrowserType = BrowserTypes.Firefox)]
public class SearchEngineTestsClassicObserver : BaseTest
{
[ExecutionBrowser(BrowserType = BrowserTypes.Chrome)]
public void SearchTextInSearchEngine_First_Observer()
{
B.SearchEngineMainPage searchEngineMainPage = new B.SearchEngineMainPage(Driver.Browser);
searchEngineMainPage.Navigate();
searchEngineMainPage.Search("Automate The Planet");
searchEngineMainPage.ValidateResultsCount("RESULTS");
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class ExecutionBrowserAttribute : Attribute
{
public ExecutionBrowserAttribute(BrowserTypes browser)
{
this.BrowserType = browser;
}
public BrowserTypes BrowserType { get; set; }
}
public class BrowserLaunchTestBehaviorObserver : BaseTestBehaviorObserver
{
public BrowserLaunchTestBehaviorObserver(ITestExecutionSubject testExecutionSubject)
: base(testExecutionSubject)
{
}
public override void PreTestInit(TestContext context, MemberInfo memberInfo)
{
var browserType = this.GetExecutionBrowser(memberInfo);
Driver.StartBrowser(browserType);
}
public override void PostTestCleanup(TestContext context, MemberInfo memberInfo)
{
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;
}
}
cus
The values from the attributes are extracted via Reflection.
var executionBrowserAttribute = memberInfo.GetCustomAttribute<ExecutionBrowserAttribute>(true);
The current observer uses the singleton class Driver to access the Web Driver configurations. In the PreTestInit phase, it tells the driver to start a new browser instance of the specified browser type. In the PostTestCleanup it calls the same class to stop and dispose the browser instance.
Throw a New Exception if There Is No Owner Attribute Set
The second observer class part of the observer design pattern is going to fail the current test if the Owner attribute is not set.
public class OwnerTestBehaviorObserver : BaseTestBehaviorObserver
{
public OwnerTestBehaviorObserver(ITestExecutionSubject testExecutionSubject)
: base(testExecutionSubject)
{
}
public override void PreTestInit(TestContext context, MemberInfo memberInfo)
{
this.ThrowExceptionIfOwnerAttributeNotSet(memberInfo);
}
private void ThrowExceptionIfOwnerAttributeNotSet(MemberInfo memberInfo)
{
try
{
var ownerAttribute = memberInfo.GetCustomAttribute<OwnerAttribute>(true);
}
catch
{
throw new Exception("You have to set Owner of your test before you run it");
}
}
}
Again the information about the test’s attribute is retrieved via Reflection. The above concrete observer is overriding only the PreTestInit method. If the method detects in this phase that there isn’t such attribute, a new exception is going to be thrown.
Extendable Test Execution in BaseTest via Observer Design Pattern
All of the previously mentioned logic should be combined together. The job is handled by the BaseTest class which is the parent class for all tests.
public class BaseTest
{
private readonly ITestExecutionSubject currentTestExecutionSubject;
private TestContext testContextInstance;
public BaseTest()
{
this.currentTestExecutionSubject = new MSTestExecutionSubject();
this.InitializeTestExecutionBehaviorObservers(this.currentTestExecutionSubject);
var memberInfo = MethodInfo.GetCurrentMethod();
this.currentTestExecutionSubject.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.currentTestExecutionSubject.PreTestInit(this.TestContext, memberInfo);
this.TestInit();
this.currentTestExecutionSubject.PostTestInit(this.TestContext, memberInfo);
}
public void CoreTestCleanup()
{
var memberInfo = GetCurrentExecutionMethodInfo();
this.currentTestExecutionSubject.PreTestCleanup(this.TestContext, memberInfo);
this.TestCleanup();
this.currentTestExecutionSubject.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(ITestExecutionSubject currentTestExecutionSubject)
{
new AssociatedBugTestBehaviorObserver(currentTestExecutionSubject);
new BrowserLaunchTestBehaviorObserver(currentTestExecutionSubject);
new OwnerTestBehaviorObserver(currentTestExecutionSubject);
}
}
If the test classes need to add its own TestInit/TestCleanup logic, they need to override the TestInit/TestCleanup methods, the TestInitialize/TestCleanup attributes should not be used. In the base CoreTestInit method first are executed the PreTestInit methods of all observers with the help of the current subject class. After that is executed the TestInit method or its overridden version. Finally, all observers PostTestInit methods are executed. The same flow is valid for the cleanup methods. In the InitializeTestExecutionBehaviorObservers are created the instances of all desired observers through passing them the current subject as a parameter. After the base constructor is executed the TestContext property is populated from the MSTest execution engine. It is used to retrieve the currently executed test’s MemberInfo.
var memberInfo = this.GetType().GetMethod(this.TestContext.TestName);
If needed similar methods can be created for the class level initializations and cleanups.
