软件测试自动化之- 基于反射的UI自动化测试框架 - UI Automation Test Framework
测试自动化程序的任务
基于反射的ui测试自动化程序,要完成的6项任务:
- 通过某种方式从测试套件程序中运行待测程序(AUT: Applicaton Under Test),以便于两个程序之间进行通信
- 操纵应用程序的窗体,从而模拟用户对窗体所实施的moving和resizing操作
- 检查应用程序窗体,确定应用程序的状态是否准确
- 操纵应用程序控件的属性,从而模拟用户的一些操作,比如模拟在一个TextBox控件里输入字符
- 检查应用程序控件的属性,确定应用程序的状态是否准确
- 调用应用程序的方法,从而模拟一些用户操作,比如模拟单击一个按钮
待测程序
AUT是一个剪刀、石头、布的猜拳软件,当点击button1时,会在listbox中显示谁是胜者。
图1 待测程序GUI
AUT代码如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace AUTForm { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { string tb = textBox1.Text; string cb = comboBox1.Text; if (tb == cb) { listBox1.Items.Add("Result is a tie"); } else if (tb == "paper" && cb == "rock" || tb == "rock" && cb == "scissors" || tb == "scissors" && cb == "paper") { listBox1.Items.Add("The TextBox wins"); } else { listBox1.Items.Add("The ComboBox wins"); } } private void menuItem2_Click(object sender, EventArgs e) { Application.Exit(); } } }
测试程序
启动待测程序
要使用反射技术通过UI来测试Windows窗体,必须要在测试套件所在的进程内创建一个单独的线程来运行被测程序。这样,测试程序和被测程序就会在运行在同一进程里面,从而可以相互进行通信。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; using System.Reflection; using System.Threading; using System.Diagnostics; using System.Drawing; namespace AUTFormTest { class Program { [STAThread] static void Main(string[] args) { try { Console.WriteLine("\nStarting test scenario"); Console.WriteLine("\nLaunching Form1"); Form theForm = null; string formName = "AUTForm.Form1"; string path = @"E:\wicresoft\Management\Knowledge\Learned\TestFramework\TestSampleLearn\AUTForm\bin\Debug\AUTForm.exe"; theForm = LaunchApp(path, formName); Console.WriteLine("\nMoving Form1"); Point pt = new Point(320, 100); Thread.Sleep(3000); SetFormProperty.SetFormPropertyValue(theForm, "Location", pt); Console.WriteLine("\nSetting textBox1 to 'rock'"); Thread.Sleep(3000); SetControlProperty.SetControlPropertyValue(theForm, "textBox1", "Text", "rock"); Console.WriteLine("Setting comboBox1 to 'scissors'"); Thread.Sleep(3000); SetControlProperty.SetControlPropertyValue(theForm, "comboBox1", "Text", "scissors"); Console.WriteLine("\nClicking button1"); object[] parms = new object[] { null, EventArgs.Empty }; Thread.Sleep(3000); InvokeClickMethod.InvokeMethod(theForm, "button1_Click", parms); bool pass = true; Console.WriteLine("\nChecking listBox1 for 'TextBox wins'"); Thread.Sleep(3000); ListBox.ObjectCollection oc = (ListBox.ObjectCollection) GetControlProperty.GetControlPropertyValue(theForm, "listBox1", "Items"); string s = oc[0].ToString(); if (s.IndexOf("TextBox wins") == -1) pass = false; if (pass) Console.WriteLine("\n-- Scenario result = Pass --"); else Console.WriteLine("\n-- Scenario result = *FAIL* --"); Console.WriteLine("\nClicking File->Exit in 3 seconds"); Thread.Sleep(3000); InvokeClickMethod.InvokeMethod(theForm, "menuItem2_Click", parms); Console.WriteLine("\nEnd test scenario"); } catch (Exception ex) { Console.WriteLine("Fatal error: " + ex.Message); } } /// <summary> /// Lunch App /// </summary> /// <param name="path">The Application path</param> /// <param name="formName">The Form name</param> /// <returns>Form Instance</returns> static Form LaunchApp(string path, string formName) { //1. Load assmebly //2. Get the define type //3. Create type instance //通过assembly读取程序,然后程序获取窗体类型,通过程序创建窗体的实例. Form result = null; Assembly a = Assembly.LoadFrom(path); Type t = a.GetType(formName); result = (Form)a.CreateInstance(t.FullName); AppState aps = new AppState(result); ThreadStart ts = new ThreadStart(aps.RunApp); Thread thread = new Thread(ts); //single thread thread.SetApartmentState(ApartmentState.STA); thread.IsBackground = true; thread.Start(); return result; } } public class AppState { public readonly Form formToRun; public AppState(Form f) { this.formToRun = f; } public void RunApp() { Application.Run(formToRun); } } }
设置窗体的属性
public static class SetFormProperty { delegate void SetFormPropertyValueHandler(Form f, string propertyName, object newValue); public static void SetFormPropertyValue(Form f, string propertyName, object newValue) { if (f.InvokeRequired) { Delegate d = new SetFormPropertyValueHandler(SetFormPropertyValue); object[] o = new object[] { f, propertyName, newValue }; f.Invoke(d, o); return; } else { Type t = f.GetType(); PropertyInfo pi = t.GetProperty(propertyName); pi.SetValue(f, newValue, null); } } }
问题1:如果在测试程序中直接调用PropertyInfo.SetValue()会抛错:"Exception has been thrown by the target of an invocation."。这是因为,不是在窗体的主线程里调用,而是在自动化测试程序所创建的一个线程里调用。因此,我们用Form.Invoke()方法以间接的方式调用SetValue。间接的方式调用,就是用delegate对象调用SetValue()。
获取窗体的属性
public static class GetFormProperty { delegate object GetFormPropertyValueHandler(Form f, string propertyName); public static object GetFormPropertyValue(Form f, string propertyName) { if (f.InvokeRequired) { Delegate d = new GetFormPropertyValueHandler(GetFormPropertyValue); object[] o = new object[] { f, propertyName }; object iresult = f.Invoke(d, o); return iresult; } else { Type t = f.GetType(); PropertyInfo pi = t.GetProperty(propertyName); object result = pi.GetValue(f, null); return result; } } }
设置控件的属性
public static class SetControlProperty { delegate void SetControlPropertyValueHandler(Form f, string controlName, string propertyName, object newValue); public static void SetControlPropertyValue(Form f, string controlName, string PropertyName, object newValue) { if (f.InvokeRequired) { Delegate d = new SetControlPropertyValueHandler(SetControlPropertyValue); object[] o = new object[] { f, controlName, PropertyName, newValue }; f.Invoke(d, o); } else { Type t1 = f.GetType(); FieldInfo fi = t1.GetField(controlName, BindingFlagsList.Flags); object ctr1 = fi.GetValue(f); Type t2 = ctr1.GetType(); PropertyInfo pi = t2.GetProperty(PropertyName); pi.SetValue(ctr1, newValue, null); } } }
BingFlags对象是用来过滤System.Reflection命名空间里许多不同类型的方法的。定义如下:
public static class BindingFlagsList { public static BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; }
获取控件的属性
public static class GetControlProperty { delegate object GetControlPropertyValueHandler(Form f, string controlName, string propertyName); public static object GetControlPropertyValue(Form f, string controlName, string propertyName) { if (f.InvokeRequired) { Delegate d = new GetControlPropertyValueHandler(GetControlPropertyValue); object[] o = new object[] { f, controlName, propertyName }; object iResult = f.Invoke(d, o); return iResult; } else { Type t1 = f.GetType(); FieldInfo fi = t1.GetField(controlName, BindingFlagsList.Flags); object ctr1 = fi.GetValue(f); Type t2 = ctr1.GetType(); PropertyInfo pi = t2.GetProperty(propertyName); object gResult = pi.GetValue(ctr1, null); return gResult; } } }
方法调用
public static class InvokeClickMethod { static AutoResetEvent are = new AutoResetEvent(false); delegate void InvokeMethodHandler(Form f, string methodName, params object[] parms); public static void InvokeMethod(Form f, string methodName, params object[] parms) { if (f.InvokeRequired) { Delegate d = new InvokeMethodHandler(InvokeMethod); f.Invoke(d, new object[] { f, methodName, parms }); are.WaitOne(); } else { Type t = f.GetType(); MethodInfo mi = t.GetMethod(methodName, BindingFlagsList.Flags); mi.Invoke(f, parms); are.Set(); } } }
问题2:假如测试套件触发了待测程序的某个方法,而这个方法直接或间接创建一个新的线程去执行。如果需要等新线程执行结束以后才能在测试套间里继续下一步操作:
- 如果知道时间暂停长度,可以使用Thread.Sleep().
- 如果没有办法知道具体要暂停多长时间, 更好的解决方案可用AutoResetEvent对象来进行同步. 当需要暂停自动化测试程序的时候,就可以插入are.WaitOne()语句, 这个语句会把AutoResetEvent对象的值设为未设置.当前线程会暂停执行, 直到are.Set()语句把AutoResetEvent对象的值被设为已设置.
结果
所有源代码如下
http://pan.baidu.com/s/1cCSqE