Introduction
In my articles from the series “Design Patterns in Automation Testing“, I am sharing with you ideas how to integrate the most useful code design patterns in the automation testing. This type of integration brings more maintainable and extendable code through the following of the SOLID principles. Most of the techniques follow the Open Close Principle where the code should be open for extension but closed for modification. I am going to use this principle in the current publication heavily. I am going to explain to you how to create an extendable test execution engine for Web Driver utilizing the classical implementation of the Observer Design Pattern. I am going to present to you even better implementations using .NET built-in event\delegates and one with IObserver<T>
. You will be able to find them in the next Advanced Observer Design Pattern article.
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.
- Strive for loosely coupled designs between objects that interact.
- Allows you to send data to many other objects in a very efficient manner.
- No modification is needed 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
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
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.
With the Observer Design Pattern, the Subject is the object that contains the state and controls it. So, there is one subject with a state. The observers, on the other hand, use the state, even if they don’t own it. There are many observers, and they rely on the Subject to tell them when its state changes. So there is a relationship between the one Subject to the many Observers.
The first step to the integration of the Observer Design Pattern in the automation test framework is to create the ISubject
interface.
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);
}
The first two methods Attach
and Detach
are used by the observer classes to associate themselves with the subject. The rest of the interface’s methods are called in the different steps of the test execution workflow to notify the observers about the changes in the state of the subject.
In order the subject to be able to notify the observers, they need to implement the IObserver
interface where all notification methods are defined.
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);
}
}
}
The specific subject knows nothing about the particular implementations of the observers. He is working with a list of ITestBehaviorObserver
. The Attach
and Detach
methods add and remove observers to/from the collection. In this classic implementation of the observer design pattern, the observers are responsible to associate themselves with the subject class.
For the current use case, not all of the observers need to implement all of the notification methods. For example, the observer for the Owner attribute needs to execute code only for PostTestCleanup
. In order to support this requirement, we are going to add a base class that is going to implement the ITestBehaviorObserver
interface.
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.
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.
[TestClass]
[ExecutionBrowser(BrowserType = BrowserTypes.Firefox)]
public class BingTestsClassicObserver : BaseTest
{
[TestMethod]
[ExecutionBrowser(BrowserType = BrowserTypes.Chrome)]
public void SearchTextInBing_First_Observer()
{
B.BingMainPage bingMainPage = new B.BingMainPage(Driver.Browser);
bingMainPage.Navigate();
bingMainPage.Search("Automate The Planet");
bingMainPage.ValidateResultsCount("RESULTS");
}
}
In the above example, the new attribute is used to configure the test engine to use the Chrome browser for all tests in the class. However, the test level attribute is going to override the class level one. So for the SearchTextInBing_First_Observer
test the Web Driver browser is going to be set to Firefox.
There is nothing special about the ExecutionBrowserAttribute
, it only holds a property of the enum BrowserTypes
.
[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; }
}
This attribute is configured to be available on test and class level through the AttributeUsage
attribute.
The logic that extracts the current values of the new attribute and the configuration of the Web Driver engine is implemented in a new concrete TestBehaviorObserver.
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;
}
}
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.
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.
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.
[TestClass]
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;
}
}
[ClassInitialize]
public static void OnClassInitialize(TestContext context)
{
}
[ClassCleanup]
public static void OnClassCleanup()
{
}
[TestInitialize]
public void CoreTestInit()
{
var memberInfo = GetCurrentExecutionMethodInfo();
this.currentTestExecutionSubject.PreTestInit(this.TestContext, memberInfo);
this.TestInit();
this.currentTestExecutionSubject.PostTestInit(this.TestContext, memberInfo);
}
[TestCleanup]
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.
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
CodeProject
The post- Observer Design Pattern- Design Patterns 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