Introduction
Here, I will present to you the second version of the Failed Tests Analysis engine part of the Design Patterns in Au?tomated 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 a more easy to use solution, so I developed the second version of the engine using the Ambient Context Design Pattern.
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
Participants
The classes and objects participating in the Ambient Design Pattern are:
TestExceptionsAnalyzerContext
- The main class that does 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. ConcreteHandler
- Holds the actual logic for handling a request. It has access to its successor. If it cannot manage the request itself, it 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";
}
Through 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 TestsExceptions
AnalyzerContext
base class. This particular class adds two handlers to the static
stack. The order depends on the 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.
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:\n {0}\n
Element Name: {1}.{2}\n URL: {3}\nElement 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.
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 can be initialized correctly. The handlers that are specified here won't be modified by the ambient context.
LoginTelerikTests
[TestClass,
ExecutionEngineAttribute(ExecutionEngineType.TestStudio, Browsers.Firefox),
VideoRecordingAttribute(VideoRecordingMode.DoNotRecord)]
public class LoginTelerikTests : BaseTest
{
[TestMethod]
public void TryToLoginTelerik_AmbientContext()
{
this.Driver.NavigateByAbsoluteUrl("https://www.telerik.com/login/");
using (new TestsExceptionsAnalyzerContext<EmptyEmailValidationExceptionHandler>())
{
var loginButton = this.Driver.FindByIdEndingWith<IButton>("LoginButton");
loginButton.Click();
var logoutButton = this.Driver.FindByIdEndingWith<IButton>("LogoutButton");
logoutButton.Click();
}
}
[TestMethod]
public void TryToLoginTelerik_AmbientContextWrapper()
{
this.Driver.NavigateByAbsoluteUrl("https://www.telerik.com/login/");
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.