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

Testing CAB and SCSF smart clients made easy.

By , 26 Mar 2007
 

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)

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!

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionChange dllmembersamhoov7-Sep-11 1:01 
I want change dll application, but application is running. Application is running on a terminal server.
How it do?
GeneralGood Workmembermokah2-Jul-07 6:36 
We need more old men like you.
Keep it up.
Good work.
GeneralFailure ExamplememberJoteep4-Apr-07 2:20 
Here in your "testing" example you're just testing the "ideal" route paths for your aaplication, which is just one method to test any application, i.e., the positive testing. But how would you acheive negative testing and other route path testing?
 
How will you test your exception handling, or validations?
 
Although it's a very impressive and beautiful example, however practically this scenerio will hardly work, since this way you are almost parallely creating a clone of your original application, will all the logics in it, with the fact that your "clone" application will itself be very complex in nature! Larger the application, larger the complexity and tougher the testing (using this approach). And then the need arises to create UTC's for this clone application! Big Grin | :-D
 
These kinnda UI testing will always be 1000 times more effective if its done manually. That way you can test every possible paths, exceptions and validations!
 
However please guide me if I am getting this wrong! But frankly after going through this article I really get to learn a lot and your unique approach is appreciable too, just that we also should look at the trade off! Smile | :)
 
Joteep
GeneralRe: Failure Examplememberhayrob4-Apr-07 5:37 
Thanks for your comments.
 
In my real application, I am constructing tests for what are essentially the use cases. In other words, if the use case is Add Contact, then I am verifying the business object (Contact) after each allowed action in the use case. The value to me of this approach is that I can keep the use case, business object and UI in line. Change any of these three elements, then in line with a UnitTesting approach, the test will fail. The test then has to reflect the change in the business object's behaviour or whatever.
 
In my case, that does include testing exception handling which again is quite easy since I am using the Exception Application Block.
 
Having said all of that, I agree with your comment that comprehensive testing means that the test harness can become as complex as the application itself.
 
The real value of this approach (which I confess I am still refining) is that the CAB allows you to test the UI of individual, or groups, of modules, and not just the complete application. What this means for me is that as I change anything, I run the unit tests, then run the UI tests, and gain some confidence that the application is not broken somewhere I didn't expect. This has been valuable because we are actually talking about COMPOSITE applications here.
 
I'm absolutely with you on the question of automated testing. I'm still doing lots and lots of manual tests. I've begun experimenting with some automated tests which randomly select use cases to run. This means that the UI start position for a use case may change, but the test will still test the impact on the business objects.
 
Thanks again for your comments - this is still work in progress for me.

GeneralError download linkmemberacarum27-Mar-07 3:08 
the link is broken
GeneralRe: Error download linkmemberhayrob27-Mar-07 4:14 
They are working for me!
GeneralRe: Error download linkmemberacarum28-Mar-07 2:09 
yes ...but the zip file is corrupted.
GeneralRe: Error download linkmemberhayrob28-Mar-07 3:19 
My apologies again. I've replaced it, downloaded it, unzipped it, and run it! It works.
Sorry again.

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130617.1 | Last Updated 27 Mar 2007
Article Copyright 2007 by hayrob
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid