|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionThis article describes UITestBench, a small but efficient library to implement User-Interface tests that can be run with NUnit or any other unit test framework. The source code provided with this project, a VisualStudio 2005 solution, is split into three parts/projects:
RequirementsThe following requirements/assumptions were considered during development of the UITestBench:
![]() It can be seen that the requirements are met as the diagram shows the AUT is not dependent on any test classes or other packages and the UITestBench is independent of the NUnit framework and the AUT.
TasksIn order to perform a UI test the following tasks need to be implemented:
The followng UML sequence diagram shows how a minimal case may look like:
Launching the application under testApplications using Windows Forms have to be run in an STA thread appartment (for more details see http://blogs.msdn.com/jfoscoding/archive/2005/04/07/406341.aspx). [STAThread]
static void Main() {
//Start the WinForms application...
...
}
However, when invoking the application from within a unit test, one cannot be sure what the actual
thread appartment state is. But this is no problem. As we need to create a new thread for running the application anyway (the
original test thread is used for running the commands (the test case) against the application in the new thread), we simply have to
set up this thread as an STA thread. This is done as follows:
public void StartApplication(string assemblyName, object args)
{
Assembly assembly = Assembly.Load(assemblyName);
if (assembly != null)
{
//Invoke the application under test in a new STA type thread
uiThread = new Thread(new ParameterizedThreadStart(this.Execute));
uiThread.TrySetApartmentState(ApartmentState.STA);
uiThread.Start(new ApplicationStartInfo(assembly, args));
}
else
{
throw new Exception("Assembly '" + assemblyName +"' not found!");
}
}
In the constructor of the thread a new delegate as entry point for the thread is passed. The Execute method that is passed as delegate method invokes the application's main-method
by using the EntryPoint property of the Assembly-Type object that is passed as parameter.
Actually the object passed as parameter is a helper class ApplicationStartInfo which
contains the assembly object to use as well as an argument object (e.g. as string[]) that
is passed to the applications main-method.
private void Execute(object param)
{
Console.WriteLine("UIExecute ThreadId: " + Thread.CurrentThread.ManagedThreadId);
Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
ApplicationStartInfo startInfo = (ApplicationStartInfo)param;
Assembly ass = startInfo.AssemblyToStart;
if (ass.EntryPoint.GetParameters().Length == 1)
{
ass.EntryPoint.Invoke(null, new object[] { startInfo.Arguments });
}
else
{
ass.EntryPoint.Invoke(null, new object[] { });
}
}
So launching the application from within the test fixture's SetUp method is as easy as this:
[SetUp]
public void SetUp()
{
myTestBench = new UITestBench();
myTestBench.StartApplication("FlexRayReplayScriptGenerator", new string[] { });
//Some time to start the demo app
Thread.Sleep(2000);
///Here the framework could be extended to wait for a certain dialog title to appear instead of a fixed delay...
}
Scanning the available UI elementsOnce the application is started and the first UI action can be invoked by the unit test (see next section), the open forms of the application have to be scanned for available UI controls.So how to get access to the open forms of the application? This is much easier than expected. One can simply use the static OpenForms property of the Application class:
foreach (Form openForm in Application.OpenForms)
{
//Let the name be the form id
string formId = formToScan.GetType().Name;
ScanUIElementsOfForm(openForm, formId);
}
The method ScanUIElementsOfForm now does the following steps:
private void ScanUIElementsOfForm(Form formToScan, string formId)
{
IDictionary
For each scanned element an UIElementInfo object is created which contains a WeakReference to the scanned element and to the owning form. The latter
is required for being able to invoke actions on the element. For each scanned form a UIFormInfo object is created which contains the UIElementInfo objects
of the form. The following class diagram shows this structure: The .NET class WeakReference is used to store the references to the scanned forms and elements as otherwise these would be prevented from being collected by the .NET
garbage collector even after the dialogs have been closed. A normal, strong reference, e.g. by assigning an object to a variable, prevents the referenced object from being collected. However,
when there are no other strong references to the object, the target becomes eligible for garbage collection even though it still has a weak reference.So, by using weak references the UI test does not interfere with the natural way the applications garbage collection would behave. The UIElementInfo class allows access to the WeakReferences only via the corresponding Properties which include a check if the referenced object is still alive.
If this is not the case an exception is thrown and the test case will fail.The method ScanUIElementsOfControl used in the above method recursively adds all available child controls of the passed control to the dictionary.
Therefore (and of course for being able to build a test case) each element must have a unique id. This unique id (unique within a form) is constructed using the following rules:
AccessibleName) the complete path to the control (via all parent controls) is taken as key.
This is always unique as an index is assigned to each control. For better understanding, the path name of a control is constructed of this index and the type name of the control.
private void ScanUIElementsOfControl(Control control, IDictionary
Finally it is checked if the given control is a ToolStrip as the foreach loop does not work for ToolStrips because for these the Controls property always returns an empty collection. As the ScanToolStripItems method works similar it is not elaborated in more detail at his point.For being able to construct the test cases easily the keys of the scanned elements are written to the console. So when using the NUnit GUI one can easily see the keys in the "Console.Out" tab. For the demo application the elements of the main form are identified using the following keys: Form1/SplitContainer[0]
Form1/SplitContainer[0]/SplitterPanel[0]
Form1/SplitContainer[0]/SplitterPanel[0]/ListBox[0]
Form1/SplitContainer[0]/SplitterPanel[1]
Form1/SplitContainer[0]/SplitterPanel[1]/ListBox[0]
Form1/toolStrip1
toolStrip1/Merge
Form1/menuStrip1
menuStrip1/File
File/Open text file 1...
File/Open text file 2...
File/
File/Exit
As the search for UI elements needs to be performed each time a new form is opened the algorithm can either be called manually from within the test case or it is called when the access of a UI element fails, that is the UI element id is not found. If the element is still not found after the rescan an exception is thrown and the test case fails. Performing UI actionsFor different types of UI Elements different actions can be performed. TheUITestBench offers several conveniance methods
for accessing the most frequently occuring actions, e.g. clicking a button or setting a text in a text box.
Of course, it is easy to add more conveniance methods if some action is needed frequently in your application.
The following methods are provided:
Dealing with external dialogsOne last problem when implementing a UI unit test is the occurrence of external, that is not user implemented or even not .NET based dialogs. An typical example for such a dialog is the "Open File" dialog, which is used by the demo application, too. An easy way to deal with such dialogs, as long as they are not too complex, is to send keystrokes to the dialog by using theSendKeys class. So to open the file dialog, select a file and open it only the following steps are required for the demo application, which uses the standard .NET file dialog:
myTestBench.PerformClick("Form1", "File/Open text file 1...");
SendKeys.SendWait("file1.txt");
SendKeys.SendWait("{ENTER}");
Finishing the testWhen a test case is finished NUnit calls theTearDown method. This is now used to call the TerminateApplication method
of the test bench which ensures that the started application is closed.
[TearDown]
public void TearDown()
{
myTestBench.TerminateApplication();
}
public void TerminateApplication()
{
Console.Write("Forcing application to shut down if it has not terminated already....");
if (uiThread != null && uiThread.IsAlive)
{
Console.WriteLine("Done!");
uiThread.Abort();
}
else
{
Console.WriteLine("not required!");
}
}
LimitationsCurrently it is not possible to have two open forms of the same type as the type name is used as unique key. This could be changed by simply appending an index to duplicate types. However this is not required for this demo project or simple applications.Using the CodeThe download contains a single Visual Studio 2005 Solution which is split up into three different projects: The demo application, the test bench framework and the test case implementation.
After opening the solution with Visual Studio, the demo application is set as start project, i.e. it can be started by simply pressing F5. ConclusionThis project has shown how it is possible to build a simple but pretty useful UI testing framework based on the standard .NET framework. The major points of interest hereby are the access to the open forms using theApplication.OpenForms property, the execution of the application under test in a separate STA type thread, the collection of the UI elements using weak references as well as the invocation of the UI elements using delegates to prevent an invalid cross thread operation to occur.
Based on this basic framework it is easy to implement additional UI testing functionality to tailor this project to specific needs. This is perfect for testing small to medium .NET applications without having to buy expensive and/or complex external solutions.
History[03.04.2008] - 1.0 - Initial release.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||