| 摘要:.NET框架组件为简单快速地建立用户界面测试自动化程序提供了一条令人惊奇的新途径。通过使用System.Reflection和System.Threading名字空间中的对象,你可以在几分钟内写好自动化测试程序。本文介绍了一个典型的基于Windows的应用程序,它将作为测试对象。接着建立了基于C#的测试工具,它模拟点击测试应用程序的UI控件并检查应用程序的状态。在工具建立后,仔细解释了它是怎样工作的,这样你就能在自己的应用中修改并扩充它。
如果你试图写一个用户界面自动化测试程序,你会发现要化很长的时间并且很棘手。通过使用Reflection和.NET框架组件中的ThreadPool对象,你能简单快速地编写强大的用户界面测试自动化程序。本文将介绍一个小的简单的基于Windows的应用程序的建立,强大的测试工具将演示这些.NET特性。
问题
假设你正在开发的Windows应用程序有标准的用户界面。Visual Studio .NET和.NET框架组件使建立按钮、菜单项和所有其它控件很简单。当然,在开发工作中你会通过检测代码中的基本功能对用户界面执行隐含的手工测试。但是假定你希望建立一个自动化的测试,它将把检查用户界面做得更全面。如果你的产品设计是稳定的,并且你有很多时间和资源,解决的方法可能是购买专门的用户界面测试软件。虽然目前有几款好的工具,但是它们都有缺点。它们相对较贵,通常使用专有的脚本语言,并且如果产品变化很大将需要花费很长时间重作脚本。由于这些原因,该方法在很多开发环境中是不适用的,必须有更好的作UI测试自动化的途径。
有了这种想法后,我开始建立一个UI测试自动化工具,它允许你在15分钟内、使用少于1页的代码建立一个测试脚本,可给新测试员使用,并且仅仅使用了.NET的功能,没有外部依赖。
在做了一些试验后我发现.NET实际上提供了建立用户界面测试自动化程序的资源,它符合所有的三个设计目标。由于解决方案实现快速,你能在产品设计经常变化的情况下建立测试自动化程序;由于解决方案容易理解,与你一起工作的人能够使用很少的时间维护测试自动化代码;由于解决方案只使用了.NET代码,没有其它的外部依赖打断测试自动化过程。
应用程序
我们将建立一个简单的应用程序作为测试的基础。启动Visual Studio .NET并建立一个叫做MyWinApp的C# Windows项目。从工具箱中添加三个按钮控件,一个文本框和一个列表框。所有控件的属性都不修改(见图1)。
图1.一个简单的应用程序
双击button1为该按钮记录一个事件处理程序并添加下面的代码,它在textBox1中显示"Hello World":
private void button1_Click(object sender, System.EventArgs e)
{
textBox1.Text = "Hello World";
}
|
你可以看到,button1_Click有两个参数。这些参数很重要,在我编写测试自动化它们模拟点击按钮。Sender参数是产生相关事件的对象--在这种情况下是button1。EventArgs参数是相关事件的附加信息。在按钮点击的情况下,没有必要的附加信息。
双击button2并添加下面的代码,它在listBox1中显示两行消息:
private void button2_Click(object sender,
System.EventArgs e)
{
listBox1.Items.Add("Goodbye World");
listBox1.Items.Add("Come back again!");
}
|
观察操作listBox1字段包含调用一个属性(Items)上的方法(Add)。当自动化时,我需要访问这些字段、属性和方法。
最后,双击button3并添加下面的代码,它删除textBox1和listBox1中的任何消息:
private void button3_Click(object sender,
System.EventArgs e)
{
textBox1.Text = "";
listBox1.Items.Clear();
}
|
建立并运行该应用程序。该应用程序有不同的状态。初始状态是{textBox1 = "textBox1", listBox1 = (empty)},图2中的状态是{textBox1 = "Hello World", listBox1 = "Goodbye World / Come back again"}。当我编写测试自动化时,我必须能够检查状态。
图2. 点击button2的状态
解决方案
现在我们建立一个简单但是强大的自动化测试工具,它模拟点击MyWinApp上的按钮,接着检查按钮的状态。启动Visual Studio .NET的一个新的实例,建立一个叫做MyWinAppTester的C# Windows应用程序。使用工具箱,添加三个按钮控件和两个文本框控件。把button1、button2和button3的text属性相应设置为LaunchApp、Invoke Method和Run Test。
这个用用户界面测试自动化程序的关键在System.Reflection和System.Threading名字空间。在代码视图中添加下面两条语句(前六条是Visual Studio .NET生成的):
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Reflection;
using System.Threading;
|
下面实现测试工具的核心。在项目的Main方法中添加图3所示的代码。在我解释这段代码前,先使测试自动化程序工作起来。双击LaunchApp按钮控件进入它的点击事件,并且双击Invoke Method和Run Test按钮。把图4中的代码添加到点击事件中。你需要在button1_Click中把MyWinApp.exe的路径修改为本机的路径。
Assembly testAssembly = null;
Form testForm = null;
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance;
static void RunApp(object state) // 需要这个函数传递进来WaitCallBack()
{
Application.Run((Form)state);
}
private void InvokeMethod(Form form, string methodName,
params object[] parms)
{
EventHandler eh = (EventHandler)Delegate.CreateDelegate(typeof
(EventHandler), form, methodName);
if (eh != null)
{
form.Invoke(eh, parms);
}
}
private object GetField(object obj, string fieldName)
{
Type t = obj.GetType();
FieldInfo fi = t.GetField(fieldName, flags);
return fi.GetValue(obj);
}
private object GetProperty(object obj, string propertyName)
{
Type t = obj.GetType();
PropertyInfo pi = t.GetProperty(propertyName, flags);
return pi.GetValue(obj, new object[0]);
}
|
图3.测试工具核心的方法和对象
// 载入MyWinApp
private void button1_Click(object sender, System.EventArgs e)
{
testAssembly = Assembly.LoadFrom("C:\\MSDN\\MyWinApp\\bin\\Debug\\MyWinApp.exe");
Type t = testAssembly.GetType("MyWinApp.Form1");
testForm = (Form)testAssembly.CreateInstance(t.FullName);
ThreadPool.QueueUserWorkItem(new WaitCallback(RunApp), testForm);
}
// 调用方法
private void button2_Click(object sender, System.EventArgs e)
{
object[] p = {this, new EventArgs()};
string meth = this.textBox1.Text.ToString();
InvokeMethod(testForm, meth, p);
}
private void button3_Click(object sender, System.EventArgs e) // 运行测试
{
object[] p = {this, new EventArgs()}; // 按钮点击的参数
bool pass = true;
for (int count = 1; count <= 3; ++count)
{
InvokeMethod(testForm, "button1_Click", p);
Thread.Sleep(1000);
if ( (string)(GetProperty(GetField(testForm,"textBox1"), "Text"))
!= "Hello World")
pass = false;
ListBox.ObjectCollection lboc = (ListBox.ObjectCollection)
GetProperty(GetField(testForm, "listBox1"), "Items");
if (lboc.Contains("Goodbye World") )
pass = false;
InvokeMethod(testForm, "button2_Click", p);
Thread.Sleep(1000);
// 检查textBox1是否为 "Hello World" 并且listBox1中有 "Goodbye World"
InvokeMethod(testForm, "button3_Click", p);
Thread.Sleep(1000);
// 检查以确性textBox1和listBox1都是空的
}
this.textBox2.Text = (pass) ? "Pass" : "Fail"; // 显示测试结果
}
|
图4.测试工具按钮行为代码
现在已经准备好建立和运行该项目了。点击LaunchApp按钮MyWinApp将被载入,如图5所示。如果点击MyWinApp上的button1、button2或button3,你会发现它的功能正常。我可以使用MyWinAppTester调用MyWinApp中单独的方法。例如,在Invoke Method按钮下的文本框中输入"button2_Click"(大小写敏感)并且点击Invoke Method按钮。这将通过调用相关的方法模拟点击MyWinApp上的button2按钮。
图5.测试程序载入该应用程序
关闭MyWinApp,点击LaunchApp按钮再次载入它,点击MyWinAppTester上的Run Test按钮。你可以看到模拟点击button1、button2、和button3,连续的点击三次。每次模拟点击后,测试检查了MyWinApp,并在最后的文本框中显示Pass或Fail消息。
如何工作的
该技术背后的核心代码如下:
private void InvokeMethod(Form form, string methodName,
params object[] parms)
{
EventHandler eh = (EventHandler)Delegate.CreateDelegate(typeof
(EventHandler), form, methodName);
if (eh != null)
{
form.Invoke(eh, parms);
}
}
|
这个方法主要是一个Form对象的Invoke方法的包装(wrapper)。该方法需要一个事件处理程序,通过使用CreateDelegate建立它,这样就使用了已有的线程而不是重新产生一个线程。了解这个方法的另一个途径是检查它的参数。为了调用一个方法,我需要知道该方法属于哪个窗体,方法的名称和该方法需要的参数。Flags变量的值如下:
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Static | BindingFlags.Instance;
|
这是几个Reflection方法所需要的过滤器。典型情况是你感兴趣的方法是私有的实例方法,但是因为 | 操作符的意思是"或",我可以捕捉到比绝对需要的要多的信息。
尽管不必要,但是拥有两个返回给定对象的字段和属性的帮助方法是很有好处的(图6所示)。在每种情况下,我获取对象的类型,获取它的信息,接着返回字段或属性。GetProperty中的GetValue调用需要一个新的object[0]参数,因为属性可能的随意地索引的。
private object GetField(object obj, string fieldName)
{
Type t = obj.GetType();
FieldInfo fi = t.GetField(fieldName, flags);
return fi.GetValue(obj);
}
private object GetProperty(object obj, string propertyName)
{
Type t = obj.GetType();
PropertyInfo pi = t.GetProperty(propertyName, flags);
return pi.GetValue(obj, new object[0]);
}
|
图6. GetField和GetProperty
载入应用程序来测试使用了两个.NET特性:Reflection和ThreadPool对象:
Assembly testAssembly = null;
Form testForm = null;
// 载入MyWinApp
private void button1_Click(object sender, System.EventArgs e)
{
testAssembly =
Assembly.LoadFrom("C:\\MSDN\\MyWinApp\\bin\\Debug\\MyWinApp.exe");
Type t = testAssembly.GetType("MyWinApp.Form1");
testForm = (Form)testAssembly.CreateInstance(t.FullName);
ThreadPool.QueueUserWorkItem(new WaitCallback(RunApp), testForm);
}
|
此处我声明了一个Assembly对象,它代表包含我将进行测试的.exe的组件(这段硬编码路径可以被"文件打开对话框"代替)。同样,我定义了一个Form对象,它代表测试应用程序的Windows窗体。为了载入该测试应用程序,我首先使用Assembly.LoadFrom获取测试组件。接着我使用GetType获取应用程序窗体的类。注意我必须使用窗体的全名。这些方法都是System.Reflection名字空间的一部分。
上面代码的最后一行执行了大多数工作。QueueUserWorkItem方法建立了执行的一个新线程。通过用它来代替建立新的线程,测试自动化程序能与测试应用程序通讯,因为两个线程运行在相同的过程中。QueueUserWorkItem需要一个WaitCallback委托,而该委托需要调用一个方法(RunApp),并且需要该方法需要的所有参数(testForm)。这段代码的基本意思是使用testForm作为参数执行RunApp方法。
因为WaitCallback委托的结构,我必须定义RunApp方法,它是Application.Run的一个简单的包装:
// 需要这个函数传递进WaitCallback()
static void RunApp(object state)
{
Application.Run((Form)state);
}
|
运行自动化测试
所有的部分完成后,我们来看一看模拟用户界面测试自动化的代码(图7所示)。
private void button3_Click(object sender, System.EventArgs e) // 运行测试
{
object[] p = {this, new EventArgs()}; // 按钮点击的参数
bool pass = true;
for (int count = 1; count <= 3; ++count)
{
InvokeMethod(testForm, "button1_Click", p);
Thread.Sleep(1000);
if ( (string)(GetProperty(GetField(testForm,"textBox1"), "Text"))
!= "Hello World")
pass = false;
ListBox.ObjectCollection lboc = (ListBox.ObjectCollection)
GetProperty(GetField(testForm, "listBox1"), "Items");
if (lboc.Contains("Goodbye World") )
pass = false;
InvokeMethod(testForm, "button2_Click", p);
Thread.Sleep(1000);
//检查textBox1是否为 "Hello World" 并且listBox1中有 "Goodbye World"
InvokeMethod(testForm, "button3_Click", p);
Thread.Sleep(1000);
//检查以确性textBox1和listBox1都是空的
}
this.textBox2.Text = (pass) ? "Pass" : "Fail"; //显示测试结果
}
|
图7.用户界面测试自动化程序
如果我希望调用button_Click方法,我必须给它传递一个对象和事件参数(EventArgs)。我从建立一个有两个项的对象数组p开始,下一步设置假定测试能够通过的Boolean变量。这些代码包含在一个for循环中,它将任意执行三次。
首先,我调用InvokeMethod,它调用button1_Click方法(它是testForm的一部分),我把p(一个发送对象和一个EventArgs对象)传递给button1_Click。简单的说,我模拟了点击button1。
Thread.Sleep(1000)是任意延迟1000毫秒(1秒)。现在,我使用GetField和GetProperty来获取testForm中的textBox1字段的Text属性,并检查看它是否是"Hello World"。简单的说,我检查了测试应用程序的状态。检查listBox1复杂一些,首先,我获取了Items属性并把它存储到变量lboc中,接着使用Contains方法检查它并确认它里面什么也没有。
同样,我模拟了点击button2和button3并检查它们的状态。如果没有找到错误的状态,通过变量的值仍然为true,否则该值将被设置为false。结果作为字符串显示在TestResult文本框中。
结论
.NET框架组件有很多经过改良的开发技巧特性。这些特性的一个"副产品"是有了一条编写测试自动化程序的强大的途径。本文演示的技术是自动化Windows用户界面测试的经验。建立测试自动化程序是有代价的,但是因为这种技术的实现如此简单,建立相对动态的产品或者资源有限制时它是理想的技术。
|