65.9K
CodeProject 正在变化。 阅读更多。
Home

CAB 和 SCSF 智能客户端测试变得轻而易举。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.76/5 (7投票s)

2007年3月27日

CPOL

7分钟阅读

viewsIcon

55669

downloadIcon

552

解释了一种测试 CAB 或 SCSF 智能客户端用户界面的方法。包括一个可重用的 CAB 模块和演示应用程序。

引言

Microsoft 模式和实践复合应用程序块 (CAB) 和智能客户端软件工厂 (SCSF) 是开发智能客户端解决方案的非常有用工具。诚然,它们需要一些学习,但努力是值得的。

这种方法的模块化意味着可以轻松地独立开发应用程序的各个元素。可以使用 NUnit 或其他单元测试框架测试这些元素。

我没有完全理解使用 CAB 测试用户界面本身是多么容易。解决方案是使用普通的 CAB 模块来运行测试(有时需要 Windows API 的帮助)。只需坐下来,看着应用程序通过您的测试运行!

本文概述了我所采取的方法。我不是世界上最伟大的程序员。如果有人能看到更简单或更好的方法,我很乐意听取他们的意见。

代码示例包括一个可重用的 CAB 模块 (TestModule) 和一个演示项目。

背景

演示应用程序非常简单。下载演示并运行可执行文件以查看应用程序的功能。

有一个主表单,允许用户选择一个联系人并查看与该联系人相关的项目,或者添加或删除一个联系人。

添加联系人会弹出一个新视图

同样,如果单击“添加项目”,则会显示一个项目视图。

该解决方案由 8 个标准 CAB 模块组成。

为了测试用户界面,只需创建一个新的业务模块。

Using the Code

像往常一样创建一个 CAB/SCSF 业务模块。此模块当然只应在测试期间加载,我选择确保它发生的方式是在应用程序启动时通过查找配置文件设置进行检查。

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

然后在 ShellApplication 中,检查配置设置。

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

我选择使用一个类来初始化应用程序,因为在它正确启动之前可能需要执行一系列任务。在我的例子中,我希望为我的业务对象设置一个数据库,以便 UI 测试针对已知配置运行。这种方法还允许正确设置 ProfileCatalog。因此,在 UITestInitialiser 类中,我们有如下代码

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

当 Shell 加载时开始测试

显然,测试不能在 Shell 完成加载之前开始。要么在 Shell 加载时引发一个特定事件,要么订阅一个做相同工作的现有事件。该事件在 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();
} 

关于这种方法有几点需要注意。首先,事件是在后台线程上调用的。如果你在 UI 线程上运行,你无法测试 UI!其次,我选择在事件触发后移除事件订阅。这可能不是必需的,但是如果你订阅了一个现有事件,你当然不希望在事件再次触发时重新开始测试。

测试在标准 CAB 服务中运行;这可能不是必需的,但它适合我并确保正确处置。

测试管理器

测试使用一个非常简单的管理器运行。

/// <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;
} 

实际工作是在各个测试中完成的。如果我们看一下添加联系人的测试,您就会明白。

测试添加新联系人

代码再次非常简单——实际工作由管理用户界面的服务完成。

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");
} 

该测试利用了 Service,即 UIService,稍后将详细介绍。该服务单击 AddContact 按钮,将联系人的名字和姓氏插入文本框并单击保存。最后,检查数据库(或者更确切地说,XML 文件)以确保 Contact 已添加。

UIService

UIService 提供了一系列方法来帮助操作用户界面。它反过来利用另一个服务 UIThreadService,该服务确保所有对 UI 控件的操作都在创建它们的线程上执行。

有一组函数用于在用户界面中查找控件。例如

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;
} 

此方法查找特定的工作区并将其作为控件返回。可能您的应用程序正在加载工作区和视图,而测试正在进行中。此方法使用一种简单但粗略的技术来确保控件确实存在,而不是在测试查找它时刚好丢失!如果控件存在,则返回它。如果不存在,线程等待 1 秒,然后重试。它这样做五次,可以相当安全地假设,在大多数应用程序中,如果一个控件在五秒后仍未加载,则它根本不存在。

有各种重载,例如

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

这会在工作区中的视图中查找特定控件。

获得控件后,您会想对它做些什么。对于文本框,您可以使用以下方法设置 Text 属性

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); 

基本的 SetControlText(cntrl,value) 方法使用 UIThread 服务来设置实际属性。

UIThread 服务必须确保控件的属性在其创建的同一线程上设置。

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(); }
} 

它只是使用控件的 Invoke 方法在 UI 线程上执行另一个方法。作为额外的一个细节,设置方法会检查控件是否绑定到数据源。如果是,则更新数据源。

其他控件也以相同的方式处理——可以选中 TreeView 节点或 ComboBox 项。可以单击 ToolStripItemsButtons 以使用户界面通过各种视图进行。

对话框和消息框

大部分内容都相当直接。运行测试的后台线程只需等待真实用户界面完成为其设置的任何任务。唯一需要解决的另一个问题是模态表单或对话框的情况,特别是非常有用的 MessageBox

当显示 MessageBox 时,您理想地希望您的测试应用程序选择表单上的相应按钮并关闭 MessageBox,以便应用程序可以继续。

实现这一点的技巧再次非常粗糙但有效。当要显示对话框时,测试应在(另一个)后台线程上执行命令,暂停测试线程片刻,然后关闭对话框。例如,当删除联系人时,会要求用户确认删除。删除联系人示例显示了一个“是/否”MessageBox。然后,测试使用 Windows API FindWindow 方法定位 MessageBox 及其按钮的句柄,并发送 Windows 消息以单击“是”或“否”按钮。(如果您需要帮助查找相应的窗口,Visual Studio Spy++ 实用程序非常有用。)

[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();
} 

关注点

我选择的语言是 VB.NET。目前,SCSF 不支持 VB.NET。鉴于我想让演示运行起来,我使用 SCSF 生成了演示应用程序,并在几个小时内让它运行起来。我没有将 TestModule 创建为 VB.NET 项目,而是决定坚持使用 C#,结果,我最终习惯了花括号和分号。而且我真的很喜欢它!下一站——C++!

当然,下一步是创建一些 GAT 方案,以便可以自动生成 TestModule 和测试类。应该很有趣!

与此同时,我正在开发的主要应用程序 www.straitonsoftware.com 历史上一直没有受益于适当的用户界面测试。嗯,现在有了!

历史

  • 创建于 2007 年 3 月 27 日
© . All rights reserved.