Click here to Skip to main content
Click here to Skip to main content

Testing CAB and SCSF smart clients made easy.

, 26 Mar 2007
Rate this:
Please Sign up or sign in to vote.
An explanation of a way to test the user interface of a CAB or SCSF smart client. Includes a reusable CAB module and demo application.

Introduction

The Microsoft Pattern and Practices Composite Application Block (CAB) and the Smart Client Software Factory (SCSF) are incredibly useful tools for developing Smart Client solutions. It is true that they take a bit of learning, but the effort is worthwhile.

The modularity of the approach means that it is easy to develop the elements of the application independently of each other. The elements can be tested using NUnit or some other unit testing framework.

I hadn't fully appreciated how easy it is to test the user interface itself using the CAB. The solution is to use a normal CAB module to run the tests (with a bit of help from the Windows API on occasions). Simply sit back and watch the application work its way though your tests!

This article outlines the approach that I have taken. I'm not the world's greatest programmer. If someone can see a simpler or better way to do it, I'd love to hear from them.

The code samples include a reusable CAB module (TestModule) and a demonstration project.

Background

The demonstration application is very simple. Download the demo and run the executable to see what the application does.

There is a main form which allows the User to select a Contact and view the Projects associated with the Contact, or to add or delete a Contact.

Adding a Contact brings up a new view:

Similarly, a Project view is displayed if Add Project is clicked.

The solution consists of 8 standard CAB modules.

In order to test the user interface, simply create a new business module.

Using the Code

Create a CAB/SCSF business module as normal. This module should be loaded of course only during testing and the way I have chosen to make sure that it happens is to check at the launch of the application by looking for a Configuration file setting.

<appSettings>
……
<add key="IsTestingUserInterface" value="true" />
……
</appSettings> 

Then in the ShellApplication, check for the config setting.

[STAThread]
static void Main()
{
string isTestingUserInterface =
    System.Configuration.ConfigurationManager.AppSettings["IsTestingUserInterface"];
if ((!string.IsNullOrEmpty(isTestingUserInterface) && (isTestingUserInterface=="true")))
{
using (UITestInitialiser initialiser=new UITestInitialiser()){initialiser.Initialise();} 

I have chosen to use a class to initialise the application because there may be a range of tasks to be performed before it is launched properly. In my case, I wish to set up a database for my business objects so that the UI tests run against a known configuration. This approach also allows the ProfileCatalog to be set up correctly. So in the UITestInitialiser class, we have code like:

// Copy the appropriate ProfileCatalog
string profileCatalog = Environment.CurrentDirectory +
InitialiseDBs();
CopyProfileCatalog(Environment.CurrentDirectory +
	"\\ProfileCatalogUITesting.xml", profileCatalog); 

Start the Test when the Shell is Loaded

Obviously the test cannot begin until the Shell has finished loading. Either raise a specific event when the Shell is loaded or subscribe to an existing event which does the same job. The event is captured in the ModuleController.

[EventSubscription(EventTopicNames.ContactSelected, ThreadOption.Background)]
public void OnContactSelected(object sender, EventArgs eventArgs)
{
// Subscribe to the event only the first time it is fired.
EventTopic topic = WorkItem.EventTopics.Get(EventTopicNames.ContactSelected);
topic.RemoveSubscription(this, "OnContactSelected");
IUITests uiTests = WorkItem.Services.AddNew<UITests, IUITests>();
uiTests.ExecuteTests();
} 

There are a couple of points to note about this approach. Firstly, the event is invoked on a background thread. There is no way you can test the UI if you are running on the UI thread! Secondly, I have chosen to remove the event subscription after the event is fired. This may not be necessary, but if you subscribe to an existing event, you certainly don't want to begin the tests again if the event is fired.

The tests are run in a standard CAB service; this may not be necessary but it suits me and makes sure that things are disposed of correctly.

The Test Manager

The tests are run using a very simple manager.

/// <summary>
/// This executes all the tests.
/// </summary>
public void ExecuteTests()
{
try
{
_result = new StringBuilder();
ExecuteTest(typeof(AddContactTest));
ExecuteTest(typeof(DeleteContactTest));
ExecuteTest(typeof(AddProjectTest));
ExecuteTest(typeof(DeleteProjectTest));
MessageBox.Show(_result.ToString(), "Test Results",
	MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show("Tests failed.\r\n" + ex.ToString(),
	"Tests failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// This executes a single test and disposes it when the test is completed.
/// </summary>
/// <param name="testType"></param>
private void ExecuteTest(Type testType)
{
ITest test = (ITest)_workItem.Services.AddNew(testType, typeof(ITest));
string txt;
if (test.Execute())
txt = testType.Name + " completed successfully\r\n";
else
txt = testType.Name + " failed\r\n";
_result.Append(txt);
_workItem.Services.Remove(typeof(ITest));
test.Dispose();
test = null;
} 

The real work is then accomplished in the individual tests. If we look at the test for adding a Contact, you will get the idea.

Test Adding a New Contact

The code is again very simple – the real work is carried out in the services which manage the user interface.

private IUIService _uiService;
[ServiceDependency]
public IUIService UIService
{ set { _uiService = value; } }
public bool Execute()
{
try
{
_uiService.ClickToolStripItem(WorkspaceNames.LayoutWorkspace,
	"_mainToolStrip", "btnAddContact");
_uiService.SetControlText(WorkspaceNames.RightWorkspace,
	"AddContactView", "txbFirstName", "Jim");
_uiService.SetControlText(WorkspaceNames.RightWorkspace,
	"AddContactView", "txbLastName", "Smith");
_uiService.ClickButton(WorkspaceNames.RightWorkspace, "AddContactView", "btnSave");
VerifyAddContact();
return true;
}
catch (UITestException ex)
{
if (MessageBox.Show(string.Format("Test: {0} failed.\r\n{1}",
	this.GetType().Name, ex.ToString()), "Test failed",
MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Error) != DialogResult.Ignore)
throw;
else
{
return false;
}
}
}
private void VerifyAddContact()
{
XmlDocument xmldoc = new XmlDocument();
xmldoc.Load(Environment.CurrentDirectory + "\\TestData.xml");
XmlNode node = xmldoc.SelectSingleNode
	("//contacts//contact[@firstName='Jim' and @lastName='Smith']");
if (node == null)
throw new UITestException("Contact not added");
} 

The test makes use of the Service, UIService, of which more later. This service clicks the AddContact button, inserts the contact's first name and last name into the textboxes and clicks Save. Finally, the database (or rather the XML file) is checked to make sure that the Contact has been added.

The UIService

The UIService provides a range of methods to help manipulate the user interface. It in turn makes use of another service UIThreadService which ensures that all actions on the UI controls are carried out on the thread on which they were created.

There are a set of functions which look for controls in the user interface. For example:

public Control GetControl(string workSpaceName)
{
// Wait for Workspace to be loaded if necessary.
int count = 5;
Control cntrl = null;
while (cntrl == null && count > 0)
{
cntrl = (Control)_workItem.Workspaces.Get(workSpaceName);
if (cntrl == null) { Thread.Sleep(1000); }
count -= 1;
}
if (cntrl == null)
throw new UITestException(string.Format
	("Not able to find Workspace: {0}", workSpaceName));
return cntrl;
} 

This method looks for a specific Workspace and returns it as a control. It may be that your application is loading Workspaces and views while the test is underway. This method uses a simple but crude technique to make sure that the control is actually there, and not just missing at the instant the test looked for it! If the control is there, it is returned. If it is not, the thread waits for 1 second and then tries again. It does this five times and it is pretty safe to assume that in most applications if a control is not loaded after five seconds, it's not there at all.

There are various overloads such as:

public Control GetControl(string workSpaceName, string view, string name) 

This looks for the specific control within a view in a workspace.

Having got a control, you then want to do something with it. For textboxes, you can set the Text property using

public void SetControlText(string workSpaceName, string view, string name, string value)
{
Control cntrl = GetControl(workSpaceName, view, name);
if (cntrl != null)
SetControlText(cntrl, value);
}
_uiThreadService.SetControlText(cntrl, value); 

The basic SetControlText(cntrl,value) method uses the UIThread service to set the actual property.

The UIThread service has to make sure that the control's property is set on the same thread it was created.

public void SetControlText(Control cntrl, string value)
{
cntrl.Invoke(new SetControlTextMethodInvoker
	(SetControlTextUIThread), new object[] { cntrl, value });
}
private void SetControlTextUIThread(Control cntrl, string value)
{
cntrl.Text = value;
if (cntrl.DataBindings.Count != 0) { cntrl.DataBindings[0].WriteValue(); }
} 

It simply uses the control's Invoke method to execute another method on the UI thread. As an added wrinkle, the setting method checks to see if the control is bound to a data source. If it is, the source is updated.

Other controls are handled in the same way – TreeView nodes or ComboBox items can be selected. ToolStripItems and Buttons can be clicked to cause the user interface to progress through the various views.

Dialogs and MessageBoxes

Most of this is pretty straight forward. The background thread on which the tests are run simply has to wait for the real user interface to complete whatever tasks have been set for it. The only other issue which needs to be addressed is the case of modal forms or dialogs, and in particular the very useful MessageBox.

When a MessageBox is displayed, you ideally want your test application to choose the appropriate button on the form and dismiss the MessageBox so that the application can proceed.

The trick to achieve this is again very crude but effective. When a dialog is going to be displayed, the test should execute the command on (another) background thread, suspend the test thread for a moment, and then dismiss the dialog. For example, when deleting a contact, the user is asked to confirm the delete. The Delete Contact example shows a Yes/No MessageBox. The test then uses the Windows API FindWindow methods to locate the handle for the MessageBox and its buttons, and sends Windows messages to click the Yes or No button. (If you need help to find the appropriate windows, the Visual Studio Spy++ utility is very helpful.)

[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr FindWindowEx(IntPtr hwndParent,
	IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("user32.dll")] // used for button-down & button-up
static extern int PostMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);
public IntPtr PointerToWindow(string windowCaption)
{
return FindWindow(null, windowCaption);
}
public IntPtr PointerToButton(IntPtr ptrToWindow, string buttonCaption)
{
//return FindWindowEx(ptrToWindow, IntPtr.Zero, null, buttonCaption);
IntPtr ptrToButton= FindWindowEx(ptrToWindow, IntPtr.Zero, null, buttonCaption);
return ptrToButton;
}
public void ClickButton(IntPtr ptrToButton)
{
uint WM_LBUTTONDOWN = 0x0201;
uint WM_LBUTTONUP = 0x0202;
PostMessage(ptrToButton, WM_LBUTTONDOWN, 0, 0); // button down
PostMessage(ptrToButton, WM_LBUTTONUP, 0, 0); // button up
Application.DoEvents();
} 

Points of Interest

My language of choice has been VB.NET. At the present time, the SCSF does not support VB.NET. Given that I wanted to get the demo up and running, I used the SCSF to generate the demo application, and I had it up and running in a couple of hours. Rather than creating the TestModule as a VB.NET project, I decided to stick with C# and, blow me down, I eventually got used to the curly brackets and semi colons. And I actually enjoyed it! Next stop – C++!

The next step of course is to create some GAT recipes so that the TestModule and the test classes can be generated automatically. Should be fun!

In the meantime, the main application I am working on www.straitonsoftware.com has historically not had the benefit of proper user interface testing. Well it does now!

History

  • Created 27 March 2007

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

hayrob
Web Developer
United Kingdom United Kingdom
Late 50s - retired after 30 years in sales, marketing, business development and general management.
Started programming three years ago - well restarted really - used to program message switching systems running on PDP-8s in the early 1970s.
Enjoying the intellectual challenge, and I think I am very good at it. The problem is that I am old and the young bucks can program much quicker than me!

Comments and Discussions

 
QuestionChange dll Pinmembersamhoov7-Sep-11 1:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140821.2 | Last Updated 27 Mar 2007
Article Copyright 2007 by hayrob
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid