下载本文的代码: TestRun0501.exe (131KB)
本页内容
手动用户界面测试是一种最基本的软件测试类型,大多数软件工程师首次采用的就是这种测试类型。与此矛盾的是,自动化用户界面测试可能是编写的测试类型中最具技术挑战的一种。Microsoft® .NET 环境为您提供了许多编写自动用户界面测试自动化的方式。一种常见而有用的方法是记录击键、鼠标移动和单击,然后在应用程序中回放以确保它以预期方式执行。(有关这种方法的详细信息,请参见 MSDN®Magazine 2002 年 3 月号中 John Robbins 的 Bugslayer 专栏。这一期 MSDN Magazine 的 Paul DiLascia 专栏也阐释了如何使用 .NET 将这种类型的输入发送到另一个应用程序中。)在本月的专栏中,我将探讨为 .NET 应用程序编写轻量级 UI 测试自动化的另一种方法。
最好的方式是以一个屏幕快照开始进行讨论。图 1 显示我有一个虚拟应用程序要进行测试。它是一个颜色合成器应用程序,允许用户在文本框控件中键入一种颜色,然后在 Combobox 中键入或选择一种颜色,单击按钮,Listbox 就会显示一条消息,表明两种颜色“混合”的结果。在图 1 中,根据应用程序,红色和蓝色会产生紫色。UI 测试自动化是一个控制台应用程序,它启动一个待测试窗体,模拟用户移动应用程序窗体,定义和调整应用程序窗体的大小,设置文本框和 Combobox 控件的值,并单击按钮控件。测试自动化检查测试应用程序的结果状态,验证 Listbox 控件包含正确的消息,并记录“pass”结果。图 1 中的屏幕快照是在测试自动化模拟用户单击关闭测试应用程序的 File | Exit 之前捕获的。
图 1 窗体 UI 测试自动化
在下面的章节中,我将简要介绍我所测试的虚拟应用程序,解释如何使用反射和 System.Windows.Forms.Application 类启动测试自动化程序中的应用程序窗体,介绍如何使用 System.Reflection 命名空间中的方法模拟用户操作和检查应用程序状态,并描述如何扩展和修改测试系统来满足自己的需要。我想,不管您在软件生产环境中扮演什么样的角色,具备快速编写轻量级 UI 测试自动化的能力都能使您的技能得到很大提高。另外,即使您正在使用一个已有的框架(如 Nunit),这些相同的技术也可以整合到您自己的单元测试管理中并相关。
待测试应用程序
让我们来看一下待测试应用程序,以便理解测试自动化的目标。待测试颜色合成器应用程序是一个简单的 Windows® 窗体。应用程序的代码是使用 C# 编写的,但我将向您介绍的 UI 自动化技术适用于用任何以 .NET 为目标的语言编写的应用程序。我接受 Visual Studio® .NET 默认控件名称 Form1、textBox1、comboBox1、button1 和 listBox1。当然,在实际的应用程序中,您应该更改控件的名称来反映它们的功能。我添加了三个虚拟菜单项:File、Edit 和 View。图 2 中列出的代码是待测试应用程序的主要内容。
当用户单击 button1 控件时,应用程序就会获取 textBox1 和 comboBox1 控件中的值。如果这两个颜色字符串匹配,就会显示这种颜色的消息。如果文本框和 Combobox 控件分别包含“red”和“blue”,则显示结果消息“purple”。如果文本框和 Combobox 控件中为其他任何颜色组合,则显示结果消息“black”。因为这只是用于演示的虚拟应用程序,我想让代码尽可能简短,所以没像在实际应用程序中那样检查输入参数。虽然这个应用程序非常简单,但它具备了演示自动化 UI 测试所需要的基于 Windows 应用程序的大多数基本特征。
即使对于如此小的应用程序,要手动测试它的用户界面也是很繁琐、易出错、耗时且又低效的。您必须键入一些输入,单击按钮控件,直观验证结果消息,并将结果记录到 Excel 电子表格或其他数据存储中。因为应用程序接受用户在文本框控件中的自由输入,实际上可能的测试输入是无限的,所以您必须测试上百甚至上千个输入才能很好地理解应用程序的行为。对于上述所有操作,一旦应用程序代码有所更改,您就必须从头执行相同的手动测试。编写单元测试是一个更好的方法,因为这些测试允许您模拟使用该应用程序的用户,然后确定应用程序是否正确响应。
测试自动化脚本
图 3 显示了测试自动化管理的整体结构,图 4 显示了代码大纲。这里我使用了 C#,但您可以很容易地将代码修改为任何基于 .NET 的语言。
图 3 UI 测试自动化结构
我首先添加和声明 System.Windows.Forms、System.Reflection 和 System.Threading 命名空间的引用。因为默认情况下控制台应用程序不引用 System.Windows.Forms.dll,所以要使用这个命名空间中的类,就需要添加对 System.Windows.Forms.dll 文件的项目引用。System.Windows.Forms 命名空间包含 Forms 类和 Application 类,它们在这个解决方案中都有使用。我使用 System.Reflection 中的类来获取和设置窗体控件的值并调用与该窗体相关的方法。使用 System.Threading 中的方法来从控制台应用程序测试管理启动窗体。
我声明了三个类作用范围对象,因为在测试管理中有几个方法要用到它们:
static Form testForm = null;
static BindingFlags flags = BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance;
static int delay = 1500;
因为颜色合成器应用程序只是一个 Windows 窗体,所以我声明一个 Form 对象来表示它。System.Reflection 命名空间中有许多方法将 BindingFlags 对象作为筛选器使用。我为 Thread.Sleep 方法设置了一个值为 1500(毫秒)的整型延时变量,这样在测试自动化的各个时刻都可以暂停 1.5 秒。我使用这段代码来启动待测试应用程序:
Console.WriteLine("\nLaunching WinApp under test");
string exePath = "C:\\FormUIAutomation\\WinApp\\bin\\Debug\\WinApp.exe";
testForm = LaunchApp(exePath, "WinApp.Form1");
我定义了图 5 中的 LaunchApp 方法及其 helper 方法 RunApp。这段代码的行数不多,但作用很大。请注意,为了简单起见,我硬编码了指向待测试应用程序可执行文件的路径(在您自己的测试中,您可能想用参数表示这个信息以使得测试自动化更加灵活)。LaunchApp 方法接受应用程序可执行文件的路径和应用程序窗体的名称,并返回一个表示该窗体的对象。LaunchApp 使用 Assembly.LoadFrom 静态方法创建 Assembly 对象的实例,而不是通过显式调用构造函数来创建。
接下来,Assembly.GetType 方法返回一个表示应用程序窗体的类型,然后我使用 Assembly.CreateInstance 方法创建对待测试窗体的引用。然后我发起一个新的线程来实际启动应用程序窗体。Application.Run 方法开始在当前线程进行消息循环;由于我想在窗体可见时执行工作,所以我需要使 Application.Run 在自己的线程中运行,这样循环才不会阻止我的进程。通过使用这种技术,测试自动化控制台应用程序管理和窗体就会在不同线程但相同的进程下运行。这种方式可以使它们相互通信 — 也就是说,测试管理可以发送指令给 Windows 窗体。
操作待测试应用程序
当启动待测试应用程序后,我模拟用户操作应用程序窗体。示例测试方案是从移动窗体和调整窗体大小开始的,如下所示:
Console.WriteLine("Moving form");
SetFormPropertyValue("Location", new System.Drawing.Point(200,200));
SetFormPropertyValue("Location", new System.Drawing.Point(500,500));
Console.WriteLine("Resizing form");
SetFormPropertyValue("Size", new System.Drawing.Size(600,600));
SetFormPropertyValue("Size", new System.Drawing.Size(300,320));
SetFormPropertyValue 方法执行了所有工作(请参见图 6)。我使用 Object.GetType 方法创建表示应用程序窗体的 Type 对象,然后使用该对象获得引用窗体属性(例如 Location 属性或 Size 属性)的 PropertyInfo 对象。一旦拥有属性信息对象,就可以使用 PropertyInfo.SetProperty 方法来操作它。SetProperty 接受三个参数。前两个可能是您想要的 — 对包含要更改属性的对象的引用和对新属性值的引用。
第三个参数是必需的,因为有些属性(例如 Listbox 控件的 Items 属性)是索引的。我这里所做的移动窗体和改变窗体大小实际上与测试应用程序功能并不相关,但我想通过它告诉您如何实现以防您的测试方案需要。还要注意,我正在使用的是 Form 类(实际上是它的 Control 基类)公开的 ISynchronizeInvoke 接口。您应该只通过拥有控件底层窗口句柄的线程访问(包含 Form)控件的属性。对于待测试窗体,该线程就是为运行 Application.Run 而发起的线程。由于我的测试管理是在单独的线程中运行的,所以我需要将对控件的属性和方法的访问封送到该线程,使控件的 Invoke 方法和 InvokeRequired 属性成为一体(该方法和属性都是 ISynchronizeInvoke 接口的一部分)。有关 IsynchronizeInvoke 的更多信息,请参阅 MSDN Magazine 2003 年 2 月号中 Ian Griffith 的文章:Give Your .NET-based Application a Fast and Responsive UI with Multiple Threads。
现在我准备模拟用户在文本框控件中键入一种颜色,并从 Combobox 控件选择一种颜色:
Console.WriteLine("\nSetting textBox1 to 'yellow'");
SetControlPropertyValue("textBox1", "Text", "yellow");
Console.WriteLine("Setting textBox1 to 'red'");
SetControlPropertyValue("textBox1", "Text", "red");
Console.WriteLine("Selecting comboBox1 to 'green'");
SetControlPropertyValue("comboBox1", "SelectedItem", "green");
Console.WriteLine("Selecting comboBox1 to 'blue'");
SetControlPropertyValue("comboBox1", "SelectedItem", "blue");
我先将 textBox1 设置为“yellow”,再将其设置为“red”,然后将 comboBox1 设置为“green”,再设置为“blue”。所有的实际工作都是由图 7 中所示的 SetControlPropertyValue 方法完成的。
我使用 Thread.Sleep 方法来暂停测试自动化以确保待测试应用程序已启动并运行。创建表示应用程序窗体类型的 Type 对象之后,我使用 Type.GetField 方法检索 Form 对象中指定字段(控件)的信息。然后调用 FieldInfo.GetType 方法获得表示想要操作的控件的 Type 对象。一旦拥有控件对象,我就可以像操作 Form 对象那样操作该控件对象,即获取控件的 PropertyInfo,然后调用 SetValue 方法。对于 SetFormPropertyValue,需要确保所有的属性更改都是在正确的线程中进行的。
请注意,测试自动化并不直接模拟极低水平的用户操作。例如,自动化并不模拟对 textBox1 控件的各次击键,而是直接设置 Text 属性。同样,自动化不模拟对 comboBox1 控件的点击,而是设置 SelectedItem 属性。这是我的测试自动化系统的一个设计缺陷。要采取那种方式测试,您可以按照 John Robbins 在前面提到的文章中的建议来做。
模拟用户在文本框和 Combobox 控件中键入颜色的操作之后,自动化会模拟点击按钮控件:
Console.WriteLine("Clicking button1");
InvokeMethod("button1_Click", new object[]{null, EventArgs.Empty} );
我已经定义了如图 8 所示的 InvokeMethod 方法。
InvokeMethod 通过调用 Object.GetType 方法获得表示待测试应用程序窗体的 Type 对象。然后我使用 Type.GetMethod 获得指定方法的信息,并调用 MethodBase.Invoke 执行指定方法。Invoke 接受两个参数。第一个是要对其调用方法的窗体实例,第二个是方法的参数数组。对于按钮控件单击方法这种情况,签名如下所示:
private void button1_Click(object sender, System.EventArgs e)为了满足 button1_Click 方法的参数需要,我需要传递一个表示发送方的对象和一个表示可选事件数据的 EventArgs 对象。对于按钮单击,我忽略了第一个参数的值,虽然对于实际的测试系统,我应该将控件作为导致调用该方法的发送方加以传递(该方法的实现可能依赖于对控件的访问,如果这个事件处理程序方法被当作多个控件的处理程序使用,则这个信息就特别有用)。对于第二个参数,我传递一个空的 EventArgs 对象。
请注意,测试自动化是通过直接调用按钮控件的相关方法模拟按钮单击的,而不是通过触发事件模拟。当实际用户单击按钮时,它会生成一个 Windows 消息,该控件对该消息进行处理,并将其转换成托管事件。这个事件会导致调用一个特定的(或一组)方法。所以如果应用程序为按钮单击事件关联了错误的方法,UI 测试自动化不会捕捉到逻辑错误(虽然每次测试都会失败,而且您会很快发现问题)。这个问题可以纠正,方法是获取使用反射的事件的底层多路广播委托,然后当事件引发时使用委托的 GetInvocationList 方法来获得要调用的所有委托的列表。然后可以单独调用每个委托。或者可以使用事件的 EventInfo 及其 GetRaiseMethod 方法来获取引发事件的方法的 MethodInfo,但这样做只返回一个自定义的引发方法,而且支持自定义引发方法的 Microsoft 语言只有 C++ 和 Microsoft 中间语言 (MSIL)。再次说明,所有这些问题都可以通过使用前面讨论的 send keys 方法加以避免。
检查应用程序状态
在自动化通过模拟用户键入和单击来设置应用程序窗体的状态之后,就可以检查系统状态来查看应用程序是否正确响应了(请参见图 9)。
我设置了一个名为“pass”的布尔变量,将其设为 true — 我假设应用程序的状态是正确的并检查该状态,如果有某个地方出错,则将 pass 设为 false。检查 textBox1 控件,确保其 Text 属性正确地设为“red”。然后检查以确保 comboBox1 的值为“blue”,而且 listBox1 显示正确的消息“Result is purple”。如果一切检查都通过,则打印 pass 消息,否则打印 fail 消息。
检查应用程序系统状态的关键是我编码的 GetControlPropertyValue 方法,如图 10 所示。首先使用 Object.GetType 创建表示应用程序窗体的 Type 对象。然后使用 Type.GetField 获取指定控件的信息。接下来再使用 GetType 获取表示控件的 Type 对象。最后使用 GetProperty 获取控件的指定属性的信息,并使用 GetValue 方法获取控件属性值。GetValue 需要一个索引对象参数,因为属性可被索引(例如,当我试图获取 Listbox 控件的 Items 属性时)。
请注意,检查 listBox1 控件中的文本比检查 textBox1 控件中的文本要灵活一些。我使用我的 GetControlPropertyValue 方法访问 Items 属性,然后使用 Contains 方法进行检查。
检查应用程序状态并记录 pass 或 fail 结果之后,我就可以轻松地退出待测试应用程序:
Console.WriteLine("\nClicking menu File->Exit in 5 seconds . . . ");
Thread.Sleep(3500);
InvokeMethod("menuItem4_Click", new object[] {null, new EventArgs()} );
虽然当测试管理终止时待测试应用程序也会终止,因为它们是在相同的进程中运行的,而且待测试应用程序是在后台线程中运行的,但为了显式清理分配的任何系统资源,最好是显式通过测试管理退出应用程序。
讨论
如果您在 .NET 问世之前就想实现 UI 测试自动化,则实际上只有两种选择。第一,购买商业化的 UI 自动化工具。第二,使用 Microsoft Active Accessibility (MSAA) API 创建自己的 UI 自动化工具。我所介绍的系统很好地补充了另外两种策略。有几种优秀的商业化 UI 自动化工具可供您使用。这些工具的优点是它们的功能齐全。而缺点是您需要为它们付费,有一段陡峭的学习曲线,而且当您需要修改功能时不允许您访问源代码。使用 MSAA 可以让您完全控制您的自动化工具,但它需要很长的时间去学习。实际上,在我做过的几个项目中,基于 MSAA 的 UI 测试自动化很可能像待测试应用程序那样复杂!
我这里提到的自动化 UI 测试方法已经成功地在几个大中型的产品中使用。由于它可以快速轻松地实现,所以当待测试系统很不稳定时,可以早早地将它用于产品周期中。然而,由于这个 UI 测试系统相对轻量级,所以它无法应付所有可能的 UI 测试情况。您可以采用多种方式修改和扩展这一设计。因为此次介绍的意图是让读者有个初步的了解,为了更加清楚和简单,我删除了大部分错误检查并对大多数信息进行了硬编码。有必要向测试自动化添加许多错误检查代码 — 毕竟您期待的是发现错误。
根据您的生产环境,您可以对测试系统的一些部分进行参数化。在测试术语方面,我提到的系统称为测试方案 (test scenario) — 控制待测试应用程序状态的一连串操作(与测试用例 (test case) 不同,后者通常指较小的操作,如传递一个参数给方法,检查返回值。)例如,要参数化该方案,您可以创建一个如下所示的输入文件:
[set state]
textBox1:Text:yellow
textBox1:Text:red
comboBox1:SelectedItem:green
comboBox1:SelectedItem:blue
button1:button1_Click
[check state]
textBox1:Text:red
comboBox1:SelectedItem:blue
listBox1:Items:Result is purple
然后您可以让您的测试自动化读取、解析和使用该文件中的数据。您还可以使用 XML 或 SQL 作为测试方案输入数据。我所介绍的测试系统将其结果记录到命令外壳中。您可以很轻松地通过命令行将结果重定向到文本文件,或者重写自动化来直接记录结果。
下一代 Windows(代号为“Longhorn”)将引入一个代号为“Avalon”的新的表示子系统。Avalon 会将我介绍的 UI 测试自动化概念提到更高的层次。我们亟待为所有 UI 元素的自动化提供平台级别的支持,并为所有用户控件公开一致的对象模型。这样可以让开发人员和测试人员快速轻松地创建极为强大的 UI 测试自动化。本专栏中的技术是对革命性的 Avalon 的工作方式的一个提示。
在 .NET 之前,编写自动化通常是一个很耗资源的任务,而测试自动化(特别是 UI 测试自动化)通常被置于产品任务优先级列表的最底层。但有了 .NET 之后,只需要花费以前所用时间的一小部分,就可以编写出非常强大的测试自动化。
请将您要给 James 的问题和建议发送到 testrun@microsoft.com。
James McCaffrey 就职于 Volt Information Sciences, Inc.,负责对在 Microsoft 工作的软件工程师进行技术培训。他参与开发多种 Microsoft 产品,包括 Internet Explorer 和 MSN Search。您可以通过 jmccaffrey@volt.com 或 v-jammc@microsoft.com 与 James 取得联系。