Here I will present to you the second version of the Failed Tests Analysis engine part of the Design Patterns in Automated Testing Series. The first version of the engine utilized the Chain of Responsibility Design Pattern however it has some drawbacks that I will mention in this publication. I decided that we need more easy to use solution so I developed the second version of the engine using the Ambient Context Design Pattern.
Definition
Definition
Provide a place to store scope or context related information or functionality that automatically follows the flow of execution between execution scopes or domains.
UML Class Diagram
classDiagram
TestExceptionsAnalyzerContext~THandler~ --> Handler
Handler <|-- ConcreteHandler
class TestExceptionsAnalyzerContext~THandler~ {
-Stack~Handler~ scopeStack
+AddHandler(THandler handler)
+Dispose()
}
class Handler {
+Handler Successor
+HandleRequest(Exception ex)
#IsApplicable(object[] context)
}
class ConcreteHandler {
+HandleRequest(Exception ex)
}
Participants
-
TestExceptionsAnalyzerContext
The main class that do most of the work. It holds a static reference to the scope stack where the chain of handlers is kept. It contains a method for adding new handlers in front of the chain. It implements the IDisposable interface so that it can be used through using statements. For usability purposes, the classes are create as a generic type where you can specify the type of the handler that needs to be added. There are successors of this class where you can specify more than one generic type.
-
Handler
Defines an interface for handling requests. Contains the HandlerRequest method.
-
ConcreteComponent
Is the object that is going to be enhanced dynamically. It inherits the Component.
-
ConcreteHandler
Holds the actual logic for handling a request. It has access to its successor. If it cannot manage the request itself, passes the execution to its successor.
What Are the Problems That We Try to Solve?
The previous solution described in the previous article Failed Tests Аnalysis- Chain of Responsibility Design Pattern was fairly good. However, if you need to specify multiple custom test-case-specific handlers, you need to call multiple methods in the right order. So I believed that the usability of the API could be improved. I thought that it might be a good idea to use the built-in goodies of the C# language such as the using statements. Through them, the readability of the code is slightly improved. The reader can find almost immediately the different handlers’ scopes.
Ambient Context Design Pattern for Failed Tests Analysis
TestsExceptionsAnalyzerContext
public class TestsExceptionsAnalyzerContext<THandler> : IDisposable
where THandler : Handler, new()
{
private static readonly Stack<Handler> scopeStack = new Stack<Handler>();
public TestsExceptionsAnalyzerContext()
{
this.AddHandlerInfrontOfChain<THandler>();
}
public void Dispose()
{
this.MakeSuccessorMainHandler();
}
protected void AddHandlerInfrontOfChain<TNewHandler>()
where TNewHandler : Handler, new()
{
var mainApplicationHandler = UnityContainerFactory.GetContainer().Resolve<Handler>(
ExceptionAnalyzerConstants.MainApplicationHandlerName);
var newHandler = UnityContainerFactory.GetContainer().Resolve<TNewHandler>();
newHandler.SetSuccessor(mainApplicationHandler);
UnityContainerFactory.GetContainer().RegisterInstance<Handler>(
ExceptionAnalyzerConstants.MainApplicationHandlerName, newHandler);
scopeStack.Push(newHandler);
}
private void MakeSuccessorMainHandler()
{
for (int i = 0; i < this.GetType().GetGenericArguments().Length; i++)
{
var handler = scopeStack.Pop();
UnityContainerFactory.GetContainer().RegisterInstance<Handler>(
ExceptionAnalyzerConstants.MainApplicationHandlerName, handler.Successor);
handler.ClearSuccessor();
}
}
}
The TestsExceptionsAnalyzerContext is the class where all of the magic is happening. As previously mentioned, here you can find a static stack variable that holds the chain of handlers. When you create the class with new generic type, it will add a new instance of it in front of the chain because the stack is a LIFO (last-in first-out) collection. We use Unity IoC container to get the primary handler of the application though an instance name- “MAIN_APP_HANDLER”. When a new handler is moved on top of the stack, it is registered as the new main handler. Once the Dispose method of the IDisposable interface is called the new main app handler is the successor of the current primary handler. This way once you leave the using statement’s scope the created custom handler is no more valid.
ExceptionAnalyzerConstants
public class ExceptionAnalyzerConstants
{
public const string MainApplicationHandlerName = @"MAIN_APP_HANDLER";
}
Though this class we can reuse the name of the primary handler registered in Unity.
TestsExceptionsAnalyzerContext<THandler1, THandler2>
public class TestsExceptionsAnalyzerContext<THandler1, THandler2> :
TestsExceptionsAnalyzerContext<THandler1>
where THandler1 : Handler, new()
where THandler2 : Handler, new()
{
public TestsExceptionsAnalyzerContext()
{
this.AddHandlerInfrontOfChain<THandler2>();
}
}
As I mentioned earlier, there are successors of the TestsExceptionsAnalyzerContext base class. This particular class adds two handlers to the static stack. The order depends on order of the specified generic types.
TestsExceptionsAnalyzerContext<THandler1, THandler2, THandler3>
public class TestsExceptionsAnalyzerContext<THandler1, THandler2, THandler3> :
TestsExceptionsAnalyzerContext<THandler1, THandler2>
where THandler1 : Handler, new()
where THandler2 : Handler, new()
where THandler3 : Handler, new()
{
public TestsExceptionsAnalyzerContext()
{
this.AddHandlerInfrontOfChain<THandler3>();
}
}
You can add even three handlers using a single TestsExceptionsAnalyzerContext.
ExceptionAnalyzer
public static class ExceptionAnalyzer
{
public static void AnalyzeFor<TExceptionHander>(Action action)
where TExceptionHander : Handler, new()
{
using (UnityContainerFactory.GetContainer().
Resolve<TestsExceptionsAnalyzerContext<TExceptionHander>>())
{
action();
}
}
public static void AnalyzeFor<TExceptionHander1, TExceptionHander2>(Action action)
where TExceptionHander1 : Handler, new()
where TExceptionHander2 : Handler, new()
{
using (UnityContainerFactory.GetContainer().
Resolve<TestsExceptionsAnalyzerContext<TExceptionHander1, TExceptionHander2>>())
{
action();
}
}
public static void AnalyzeFor<TExceptionHander1, TExceptionHander2, TExceptionHander3>(Action action)
where TExceptionHander1 : Handler, new()
where TExceptionHander2 : Handler, new()
where TExceptionHander3 : Handler, new()
{
using (UnityContainerFactory.GetContainer().
Resolve<TestsExceptionsAnalyzerContext<TExceptionHander1, TExceptionHander2, TExceptionHander3>>())
{
action();
}
}
}
If you are not a fan of the curly brackets and using statements, this class is specially designed for you. This is a helper wrapper around the TestsExceptionsAnalyzerContext where you can specify a handler and a custom action. The specified handler will be valid only for the code of the anonymous action. I prefer the pure using statements approach.
While ago when we were working on the first version of the BELLATRIX test automation framework, I did this research while I was working on a similar feature for our solution.
Ambient Context Design Pattern in Tests
ExceptionAnalizedElementFinderService
public class ExceptionAnalizedElementFinderService
{
private readonly IUnityContainer container;
private readonly IExceptionAnalyzer excepionAnalyzer;
public ExceptionAnalizedElementFinderService(
IUnityContainer container,
IExceptionAnalyzer excepionAnalyzer)
{
this.container = container;
this.excepionAnalyzer = excepionAnalyzer;
}
public TElement Find<TElement>(IDriver driver, Find findContext, Core.By by)
where TElement : class, Core.Controls.IElement
{
TElement result = default(TElement);
try
{
string testingFrameworkExpression = by.ToTestingFrameworkExpression();
this.WaitForExists(driver, testingFrameworkExpression);
var element = findContext.ByExpression(by.ToTestingFrameworkExpression());
result = this.ResolveElement<TElement>(driver, element);
}
catch (Exception ex)
{
#region 10. Failed Tests Аnalysis- Ambient Context Design Pattern
var mainApplicationHandler = UnityContainerFactory.GetContainer().Resolve<Handler>(
ExceptionAnalyzerConstants.MainApplicationHandlerName);
mainApplicationHandler.HandleRequest(ex, driver);
#endregion
throw;
}
return result;
}
public IEnumerable<TElement> FindAll<TElement>(IDriver driver, Find findContext, Core.By by)
where TElement : class, Core.Controls.IElement
{
List<TElement> resolvedElements = new List<TElement>();
try
{
string testingFrameworkExpression = by.ToTestingFrameworkExpression();
this.WaitForExists(driver, testingFrameworkExpression);
var elements = findContext.AllByExpression(testingFrameworkExpression);
foreach (var currentElement in elements)
{
TElement result = this.ResolveElement<TElement>(driver, currentElement);
resolvedElements.Add(result);
}
}
catch (Exception ex)
{
#region 10. Failed Tests Аnalysis- Ambient Context Design Pattern
var mainApplicationHandler = UnityContainerFactory.GetContainer().Resolve<Handler>(
ExceptionAnalyzerConstants.MainApplicationHandlerName);
mainApplicationHandler.HandleRequest(ex, driver);
#endregion
throw;
}
return resolvedElements;
}
public bool IsElementPresent(Find findContext, Core.By by)
{
try
{
string controlFindExpression = by.ToTestingFrameworkExpression();
Manager.Current.ActiveBrowser.RefreshDomTree();
HtmlFindExpression hfe = new HtmlFindExpression(controlFindExpression);
Manager.Current.ActiveBrowser.WaitForElement(hfe, 5000, false);
}
catch (TimeoutException)
{
return false;
}
return true;
}
private void WaitForExists(IDriver driver, string findExpression)
{
try
{
driver.WaitUntilReady();
HtmlFindExpression hfe = new HtmlFindExpression(findExpression);
Manager.Current.ActiveBrowser.WaitForElement(hfe, 5000, false);
}
catch (Exception)
{
this.ThrowTimeoutExceptionIfElementIsNull(driver, findExpression);
}
}
private TElement ResolveElement<TElement>(
IDriver driver,
ArtOfTest.WebAii.ObjectModel.Element element)
where TElement : class, Core.Controls.IElement
{
TElement result = this.container.Resolve<TElement>(
new ResolverOverride[]
{
new ParameterOverride("driver", driver),
new ParameterOverride("element", element),
new ParameterOverride("container", this.container)
});
return result;
}
private void ThrowTimeoutExceptionIfElementIsNull(IDriver driver, params string[] customExpression)
{
StackTrace stackTrace = new StackTrace();
StackFrame[] stackFrames = stackTrace.GetFrames();
StackFrame callingFrame = stackFrames[3];
MethodBase method = callingFrame.GetMethod();
string currentUrl = driver.Url;
throw new ElementTimeoutException(
string.Format(
"TIMED OUT- for element with Find Expression: {0} Element Name: {1}.{2} URL: {3}Element Timeout: {4}",
string.Join(",", customExpression.Select(p => p.ToString()).ToArray()),
method.ReflectedType.FullName, method.Name, currentUrl, Manager.Current.Settings.ElementWaitTimeout));
}
}
The only difference compared to the previous solution here is how we get the main app handler. Instead of passing it as a parameter, this time, we resolve it through the named instance.
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
ExecutionEngineBehaviorObserver
UnityContainerFactory.GetContainer().RegisterType<FileNotFoundExceptionHandler>(
ExceptionAnalyzerConstants.MainApplicationHandlerName);
var mainHandler = UnityContainerFactory.GetContainer().Resolve<FileNotFoundExceptionHandler>();
UnityContainerFactory.GetContainer().RegisterInstance<Handler>(
ExceptionAnalyzerConstants.MainApplicationHandlerName,
mainHandler,
new HierarchicalLifetimeManager());
The above code should be added to the ExecutionEngineBehaviourObser so that the primary chain of handlers to be initialized correctly. The handlers that are specified here won’t be modified by the ambient context.
LoginProductSiteTests
[TestClass,
ExecutionEngineAttribute(ExecutionEngineType.TestStudio, Browsers.Firefox),
VideoRecordingAttribute(VideoRecordingMode.DoNotRecord)]
public class LoginProductSiteTests : BaseTest
{
public void TryToLoginProductSite_AmbientContext()
{
this.Driver.NavigateByAbsoluteUrl("productSiteUrllogin/");
using (new TestsExceptionsAnalyzerContext<EmptyEmailValidationExceptionHandler>())
{
var loginButton = this.Driver.FindByIdEndingWith<IButton>("LoginButton");
loginButton.Click();
var logoutButton = this.Driver.FindByIdEndingWith<IButton>("LogoutButton");
logoutButton.Click();
}
}
public void TryToLoginProductSite_AmbientContextWrapper()
{
this.Driver.NavigateByAbsoluteUrl("productSiteUrllogin/");
ExceptionAnalyzer.AnalyzeFor<EmptyEmailValidationExceptionHandler>(() =>
{
var loginButton = this.Driver.FindByIdEndingWith<IButton>("LoginButton");
loginButton.Click();
var logoutButton = this.Driver.FindByIdEndingWith<IButton>("LogoutButton");
logoutButton.Click();
});
}
}
The first test method uses using statements to add a new custom handler. The second one utilizes the helper wrapper.
