net自动化测试之道基于反射的UI自动化测试—激活方法

来源:互联网 发布:java中aop是什么意思 编辑:程序博客网 时间:2024/06/05 05:59

Invoking Methods

Problem

You want to invoke a method of a form-basedapplication.

问题

如何激活winform应用程序的方法?

Design

Get a reference to the method you want toinvoke using the Form.GetType()and Type.GetMethod()

methods.Then use the MethodInfo.Invoke()methodin conjunction with an AutoResetEvent

object and a method delegate to call the targetmethod.

 设计

使用Form.GetType()和Type.GetMethod()方法获取我们想激活的方法的引用.然后结合代理和AutoResetEvent使用MethodInfo.Invoke()方法调用目标方法.

 解决方案

if(theForm.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

object[]parms=newobject[]{null,EventArgs.Empty};

object[]o=newobject[]{theForm,"button1_Click",parms};

theForm.Invoke(d,o);

are.WaitOne();

}

else

{

Console.WriteLine("Unexpected logicflow");

}

在某处定义:

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

static AutoResetEvent are=newAutoResetEvent(false);

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

Type t=f.GetType();

MethodInfo mi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

are.Set();

}

注解

Comments

When writing lightweight reflection-basedUI test automation,you usually need to invoke

methods that are part of the applicationform to simulate user actions.Examples include

invoking a button1_Click()method,whichhandles actions when a user clicks on a button1

control,and invoking amenuItem2_Click()method,which handles actions when a user clicks

on a menuItem2menu item.Notice that reflection-based UI automation simulates a button

click by directly invoking the buttoncontrol’s associated method rather than by firing an

event.When a real user clicks on abutton,it generates a Windows message that is processed

by the control and turned into a managedevent.This causes a particular method to be

invoked.So,reflection-based UI automationwill not catch the logic error if the AUT has the

wrong method wired to a button click event.

The key to invoking methods is to use the MethodInfo.Invoke()method.Ifthere were no

hidden issues,you could invoke a methodlike this:

Type t=theForm.GetType();

MethodInfomi=t.GetMethod("button1_Click",flags);

mi.Invoke(theForm,newobject[]{null,EventArgs.Empty});

where

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

The BindingFlags objectis a filter for many of the methods in the System.Reflection

namespace and is discussed in Section2.4.The MethodInfo.Invoke()method accepts two

arguments.The first argument is the parentForm object that owns the method being invoked.

The second argument is an object arraycontaining the arguments that must be passed to the

method being invoked.In this example,thebutton1_Click()method has a signature of

private voidbutton1_Click(object sender,System.EventArgs e)

So you need to pass values for parameterssender and e,representing the object associ-

ated with the button1_Click()method andoptional event data the method might need.For

lightweight UI test automation,you canignore these parameters and simply pass null and

EventArgs.Empty.

As described in Sections 2.3,2.3,and2.4,there is a hidden issue—you should not call

MethodInfo.Invoke()directly from a threadthat is not the main Form thread.The solution to

this hidden invoke issue is to call MethodInfo.Invoke()indirectlythrough a Delegate object:

if(theForm.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

object[]parms=newobject[]{null,EventArgs.Empty};

object[]o=newobject[]{theForm,"button1_Click",parms};

theForm.Invoke(d,o);

}

else

{

Console.WriteLine("Unexpected logicflow");

}

where

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

Type t=f.GetType();

MethodInfomi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

}

 

This code will work most of thetime;however,programmatically invoking a method has

a second,very subtle,hidden issue involvingsynchronization.Suppose your test harness

invokes a method on the AUT,and that methoddirectly or indirectly spins off a new thread

of execution.Before your test harness takesany further action,you must wait until control

is returned to the test harness.There aretwo solutions to this timing problem.The first is a

crude but effective approach:placeThread.Sleep()statements in your test harness to slow

the automation down.For example:

if(theForm.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

object[]parms=newobject[]{null,EventArgs.Empty};

object[]o=newobject[]{theForm,"button1_Click",parms};

theForm.Invoke(d,o);

Thread.Sleep(2000);

}

else

{

Console.WriteLine("Unexpected logicflow");

}

where

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

Type t=f.GetType();

MethodInfomi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

Thread.Sleep(2000);

}

However,this crude approach has a bigproblem:there’s no way to determine how long

to pause so you must make your delay timeslong.This leads to a test harness with multiple

lengthy delays.A better solution to thetiming problem is to use an AutoResetEvent object for

synchronization.You declare a class scopeobject like

static AutoResetEvent are=newAutoResetEvent(false);

which creates an object that can have avalue of signaled or not-signaled.The false argument

means initialize the object tonot-signaled.Then,whenever you want to pause your automa-

tion,you insert the statementare.WaitOne().This sets the value of the AutoResetEvent object

to not-signaled.The current thread ofexecution halts until the AutoResetEvent object is set to

signaled from an are.Set()statement.Puttingthese ideas together led to this code:

if(theForm.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

object[]parms=newobject[]{null,EventArgs.Empty};

object[]o=newobject[]{theForm,"button1_Click",parms};

theForm.Invoke(d,o);

are.WaitOne();

}

else

{

Console.WriteLine("Unexpected logicflow");

}

where

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

static AutoResetEvent are=newAutoResetEvent(false);

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

Type t=f.GetType();

MethodInfomi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

are.Set();

}

So,at the beginning of the code,thetest-automation thread does not own the main test

Form object thread,and the InvokeRequiredproperty is true.Execution control is transferred

to the InvokeMethodHandler()delegate,whichin turn is associated with an InvokeMethod()

helper method.InvokeMethod()actuallyperforms the work by calling MethodInfo.Invoke().

For synchronization,calling AutoResetEvent.WaitOne()blocksthe thread of execution,allow-

ing the MethodInfo.Invoke()method tocomplete execution.Calling AutoResetEvent.Set()

signals that the thread of execution canresume.

You can greatly modularize this techniqueby wrapping the code in a single self-referential

method in conjunction with a delegate andan AutoResetEvent object:

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

if(f.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

f.Invoke(d,newobject[]{f,methodName,parms});

are.WaitOne();

}

else

{

Type t=f.GetType();

MethodInfomi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

are.Set();

}

}

where

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

static AutoResetEvent are=newAutoResetEvent(false);

With this convenient wrapper method you canmake clean calls:

object[]parms=new object[]{null,EventArgs.Empty};

InvokeMethod(theForm,"button1_Click",parms);

The InvokeMethod()wrapper isself-referencing.On the initial call to InvokeMethod()from

your test harness,InvokeRequired is truebecause the call is coming from your test harness.

Control of execution transfers to theForm.Invoke()method,which passes control to the

InvokeMethodHandler()delegate.The delegateis associated with the original InvokeMethod()

method,so execution control reentersInvokeMethod().The second time through the helper

method,InvokeRequired will be false,socontrol is transferred to the else block where

MethodInfo.Invoke()actually invokes themethod passed in as an argument to the helper.

当编写轻量级基于反射的UI自动化测试的时候,通常我们需要激活Form的的方法来模拟用户行为.例子中包括了激活button1_Click()方法处理当用户点击button1控件的行为,还包括激活menuItem2_Click()方法处理用户点击menuItem2菜单项的行为.注意到基于反射的UI自动化测试是直接激活与button控件关联的方法,而不是通过事件模拟点击按钮.用户真正点击一个按钮的时候,会形成由控件处理的windows消息,并且变成一个托管的事件.这将导致一个特定方法被激活.因此,当AUT存在错误的方法连接一个按钮点击事件的时候,基于反射的UI自动化是不能捕获这个逻辑错误的.

激活方法的关键是使用MethodInfo.Invoke()方法.如果没有隐藏的问题,我们可以这样激活方法: Type t=theForm.GetType();

MethodInfomi=t.GetMethod("button1_Click",flags);

mi.Invoke(theForm,newobject[]{null,EventArgs.Empty});

在某处声明:

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

BindingFlags对象是一个过滤器. MethodInfo.Invoke()接收两个参数,第一个为拥有激活方法的父Form对象,第二个参数是激活方法的参数值数组.在这个例子中, button1_Click()的签名是:

private void button1_Click(objectsender,System.EventArgs e)

因此我们需要传递sender和e,分别代表与button1_Click()关联的对象和可能需要的事件数据.对于轻量级自动化UI测试,我们可以忽略这些参数,传递null和EventArgs.Empty就行.

这里同样存在线程问题.解决方法是通过代理间接调用MethodInfo.Invoke()方法:

if(theForm.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

object[]parms=newobject[]{null,EventArgs.Empty};

object[]o=new object[]{theForm,"button1_Click",parms};

theForm.Invoke(d,o);

}

else

{

Console.WriteLine("Unexpected logicflow");

}

在某处声明:

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

Type t=f.GetType();

MethodInfomi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

}

这段代码大多数情况下可以正常工作.但是用程序激活方法还存在第二个隐藏的问题,即同步问题.假设我们的测试套件激活了AUT的一个方法,并且该方法直接或间接的创建了一个新的执行线程.在我们的测试套件继续执行之前,我们必须等到控件返回到测试套件.对于这个时间问题,有两中解决方案.第一种比较原始,但是有效.即在测试套件中加上sleep语句延迟测试套件的运行.例如:

if(theForm.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

object[]parms=new object[]{null,EventArgs.Empty};

object[]o=newobject[]{theForm,"button1_Click",parms};

theForm.Invoke(d,o);

Thread.Sleep(2000);

}

else

{

Console.WriteLine("Unexpected logicflow");

}

在某处:

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

Type t=f.GetType();

MethodInfo mi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

Thread.Sleep(2000);

}

然而,这种原始的方式存在一个很大的问题:我们不确定延迟时间该设置多长,因为必须将延迟时间设置的尽可能的长.这可能导致延迟时间可能超过实际需要等待时间的数倍.一种更好的方式是使用AutoResetEvent解决同步.我们声明一个类级变量:

static AutoResetEvent are=new AutoResetEvent(false);

创建一个对象,该对象有两种状态. 如果初始状态为终止状态,则为true;否则为false。然后,无论我们想什么时候中止套件,我们插入are.WaitOne()语句。该语句将AutoResetEvent的值设为非终止状态。当前执行线程将中止,直到are.Set()将AutoResetEvent的值设为终止状态。综合整理代码如下:

if(theForm.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

object[]parms=newobject[]{null,EventArgs.Empty};

object[]o=newobject[]{theForm,"button1_Click",parms};

theForm.Invoke(d,o);

are.WaitOne();

}

else

{

Console.WriteLine("Unexpected logicflow");

}

在某处声明:

static BindingFlagsflags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

static AutoResetEvent are=newAutoResetEvent(false);

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

Type t=f.GetType();

MethodInfomi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

are.Set();

}

在代码的开始,测试套件线程不拥有Form的线程,InvokeRequired属性为true。程序执行转移到InvokeMethodHandler()代理,从而执行与之关联的方法InvokeMethod()。为了同步,调用AutoResetEvent.WaitOne()阻塞线程,让MethodInfo.Invoke()方法完全执行。调用AutoResetEvent.Set()发送信号,释放等待线程。

将代码封装到一个自我调用的方法中可以大大增加模块性:

delegate void InvokeMethodHandler(Formf,string methodName,

params object[]parms);

static void InvokeMethod(Form f,stringmethodName,

params object[]parms)

{

if(f.InvokeRequired)

{

Delegate d=newInvokeMethodHandler(InvokeMethod);

f.Invoke(d,newobject[]{f,methodName,parms});

are.WaitOne();

}

else

{

Type t=f.GetType();

MethodInfomi=t.GetMethod(methodName,flags);

mi.Invoke(f,parms);

are.Set();

}

}

在某处声明:

static BindingFlags flags=BindingFlags.Public|

BindingFlags.NonPublic|

BindingFlags.Static|

BindingFlags.Instance;

static AutoResetEvent are=newAutoResetEvent(false);

有了这个封装的方法,我们可以很方便的调用:

object[]parms=newobject[]{null,EventArgs.Empty};

InvokeMethod(theForm,"button1_Click",parms);

InvokeMethod()是一个自我调用的方法,一开始调用来自测试套件,InvokeRequired为true。程序进入if分支,执行代理InvokeMethodHandler(),进而是与之关联的方法InvokeMethod(),重新进入InvokeMethod(),InvokeRequired为false,程序进入else分支,执行MethodInfo.Invoke()真正激活方法。

原创粉丝点击