【Abstract 】Today more and more application programs work with graphic user interface(GUI).In order to support the test of those applications,asoftware testing tool must provide the capability to capture user's operations(keystrokes,mouse activity,etc.)and then be able to automatically replaythem after ”http://www.1daixie.com/” the code has been modified for efficient regression testing of the applications.This paper discusses the technique for capturing andplaybacking Java input events.
【Key words】Software testing; Capture/playback; Java event
摘要:目前,越来越多的应用程序需要和图形用户界面一起工作,一个优秀的软件测试工具必须提供捕获(Capture)用户操作(如击键、鼠标活动等)的能力并在代码被修改之后能够自动回放(Playback) 用户的操作。文章结合自主开发的面向Java的事件捕获/回放工具JPlayer(Swing/AWT事件记录器/回放器,)介绍了基于事件源识别的捕获/回放技术,以及基于事件跟踪的回放同步技术。
关键词:软件测试;捕获/回放;Java事件
目前,越来越多的应用程序需要和图形用户界面(GUI)一起工作。一个错误时常在前一次测试运行中被发现,但是由于测试人员很难记住以前所有的步骤,致使许多GUI操作步骤不能被重复。同时,软件开发过程中为了排除故障、满足增加的需求、改善性能等,代码总是要修改的,手动重复运行测试不但费时,而且很难奏效。为了支持这些应用程序的测试,一个优秀的软件测试工具必须提供捕获(Capture)用户操作(如击键、鼠标活动等)的能力,并在代码被修改之后自动回放(Playback)用户的操作。
捕获/回放功能可以把用户在进行测试时的键盘和鼠标等输入操作记录下来,同时也把软件的响应记录下来,当对软件作了修改要重新运行这个测试时,就可以利用测试回放功能把这个测试以前所作的输入操作重新应用到本次测试中,并自动比较软件对本次测试和以前的测试的响应是否相同,如果不同,就表明对软件的修改产生了新的错误。所以测试捕获和回放工具可以简化自动回归测试的实现,大大提高测试效率,使测试更具完整性。
1 JPlayer的设计特点
传统的捕获/回放工具的工作原理利用的是绝对屏幕位置方式,但是由于应用程序的窗口运行受到窗口管理器的控制,不能保证同一窗口/GUI组件在多次运行中都出现于相同的位置,以及在应用程序多次运行过程中由于GUI组件的响应时间不一致等,都会导致回放失败[1]。
JPlayer是专门为软件开发人员设计的新一代捕获/回放工具,它通过窗口/GUI组件的标题来识别窗口,辅以相对屏幕位置管理。
2 JPlayer的软件结构
JPlayer(Swing/AWT事件记录器/回放器)是用来捕获/回放Java应用程序的输入事件的工具。被测试的应用程序的GUI组 件由Swing/AWT 组件构成,JPlayer能够回放所有捕获的GUI事 件(包括用户的击键、鼠标活动等)。JPlayer包括了事件记录器和事件回放器。
2.1事件记录器
事件记录器用自己的类装载器来装入被测的Java应用程序,然后,通过监视应用程序的系统事件队列,记录下所有的用户输入事件非曲直(包括KeyEvents 和MouseEvents)和其它系统低级事件(包括ComponentEvent 、FocusEvent、WindowsEvent和 ContainerEvent等),并按事件产生的先后顺序将事件的相关信息存储到事件记录文本文件中。图1是事件记录器的结构示意图。
对于KeyEvent 和MouseEvent,需要保存的信息除了其各自事件对象中的所有信息外,还包括了该事件与前一KeyEvent/MouseEvent事件间的间隔时间。例如:假如用户用鼠标点击了窗体Frame1 的菜单Menu1,则生成的测试脚本为:MouseClick Frame("Frame1").Menu("Menu1")鼠标的x坐标,鼠标的y坐标与前一输入事件的间隔时间鼠标点击次数对其它系统低级事件,则只记录下事件类型和事件源。
2.2事件回放器
事件回放器用自己的类装载器来装入被测的Java应用程序。然后,事件生成器从事件记录文件中按事件被记录下的顺序依次读出所有的事件。对于输入事件,先根据事件记录文件中保存的相关信息还原为原先的鼠标或键盘事件。最后,将生成的输入事件发送给对应组件。事件回放器的结构如图2所示。
通过事件监视器来同步回放过程。事件监视器通过比较截获的系统事件与记录的系统事件,判断回放过程中是否有意外发生,并控制下一个输入事件的回放时机(即不仅靠记录的事件间隔来作为控制事件回放速度的唯一依据)。
3 JPlayer关键技术的实现
3.1设计自己的类装载器
因为我们设计的事件捕获/回放器是通过监视被测应用程序(Java类)的系统事件队列来记录/回放被测程序的输入事件的[2],所以被测应用程序在运行时必须与事件捕获/回放器在同一进程内,否则,无法监视其系统事件队列。同时,被测试的Java应用程序是在运行时才能确定的,所以需要在运行时能动态地装载应用程序。但不能用以下方式来动态装载应用程序:String app="NewClass";Object o=new app;
因为通过"new"装载的类必须是在编译时已经存在的,但是不可能在编译时就确定需要在运行时装载什么类。而且即使能够这样做,也是行不通的,因为不知道该调用被装载类的哪一个构造函数。在C/C++中,程序设计人员可以很容易地利用动态链接库(LoadLibrary和 FreeLibrary)来实现在运行时加载/释放模块的功能。而Java作为一种极具动态性的语言,通过反射(Reflection)机制提供了动态类载入机制。Java 中提供此功能的包是java.lang.reflect ,其中java.lang.Class类封装了动态装载类库的功能。
以下通过一段示例程序来简单说明如何利用Class类设计我们的类装载器[3]。该段程序从命令行接收需装载的类名(为简单起见,假设被装载类没有输入参数),然后装载该类,并执行该类的main方法。
public class MyClassLoader
{
public static void main(String[]args)throws Exception
{
Class LoadCls=Class.forName(args[0]);
//从命令行参数中获取需要装载的类名
Method mainMethod=getMain(LoadCls);
//获取装载类的main方法
mainMethod.invoke(null,null);//执行装载类的main方法
}
private static Method getMain(Class cls)throws Exception
{
Method[]methods=cls.getMethods();//获得类的所有方法
for(int i=0;i<methods.length;i++)
{
if(methods[i].getName().equals("main"))
//当前方法是否main方法
return methods[i];
}
return null;
}
}
3.2截取用户输入事件
(1)Java事件的生命周期
Java事 件是由事件源产生的。事件源可以是GUI组件、Java Bean或 任何有生成事件、能力的对象。在GUI组件这种情况下,事件源可以是组件的同位体(对于AbstractWindow Toolkit[AWT]GUI组件),或者是组件本身(对于SWING组件来说)。
当操作环境响应用户的一个动作(如鼠标点击)从而生成一个事件时,同操作环境通信的AWT部分会收到一个通知,然后AWT 把它转化为一个AWT 事件。接着,AWT把此次事件添加进系统事件队列。现在,事件处于事件分派线程的控制下。分派线程依次从系统事件队列中取出每个事件,送到生成该事件的组件的分发过程(dispatchEvent()方法)。dispatchEvent()方法会调用processEvent()方法并将事件的一个引用传递给processEvent()方法。此时,系统会查看是否有已注册的该事件的监听器,如果是,processEvent()方法会调用事件特定的处理方法,事件特定的处理方法将事件送到组件的所有已注册的该事件的监听器[4]。
(2)通过系统事件队列来截取用户输入事件
从以上对Java事件生命周期的分析中可以看出,用户输入事件(KeyEvent 和MouseEvent)在事件生成之后直到dispatchEvent()方法分派事件之前,一直都是存储在系统事件队列中的,而且所有的事件都由dispatchEvent()方法来分派。所以只要能重载dispatchEvent()方法就可以获取系统的所有事件,包括用户输入事件。
Java 提供了EventQueue类来访问甚至操纵系统事件队列。EventQueue类中封装了对系统事件队列的各种操作,除了dispatchEvent()方法外,其中最关键的是提供了push()方法,允许用特定的EventQueue来 代替当前的EventQueue。只要从EventQueue 类中派生一个新类,然后通过push()方法用派生类来代替当前的EventQueue类即可。这样,所有的系统事件都会转发到派生EventQueue类。然后,再在派生类中重载dispatchEvent()方法就可以截获所有的系统事件,包括用户输入事件。可以通过ToolKit 类中定义的getSystemEventQueue()方法来获得对EventQueue的引用。
3.3如何实现事件回放
传统的事件回放器对鼠标事件的回放是利用绝对屏幕位置方式,即在捕获鼠标事件时记录下此时鼠标点击处在屏幕上的绝对位置坐标,此后回放鼠标事件时便以记录下的屏幕绝对位置坐标作为鼠标回放时的位置。这样设计的好处是回放器设计简单,但前提是每次回放时对应的窗口/组件都必须处于相同的位置,而且窗口/组件对应于鼠标点击位置的部分必须可见,不能被隐藏或其他窗口遮蔽。
但实际上,由于应用程序窗口的运行受到窗口管理器的控制,不能保证同一窗口/GUI组件在多次运行中都出现于相同的位置,而且,即使同一窗口/GUI组件在多次运行中每次出现于相同的位置,如果受其它应用程序或执行过程中某个条件失败的干扰,都会导致鼠标后续的回放失去目标。例如,其它应用程序突然弹出一个窗口,或者执行过程中打印机缺纸、申请的网页不存在等,都会导致目标回放的窗口/组件失位。鉴于此,我们设计的事件回放器通过窗口/GUI组件的标题来识别窗口,辅以相对屏幕位置管理。其设计原理是:每当捕获一个鼠标/键盘输入事件时,记录下响应该鼠标/键盘事件的窗口/组件的名称,以及鼠标相对于该窗口/组件的坐标位置。当然,还包括其它一些基本信息。因为Java中组件属于某一个容器(包括窗口),而容器又可以嵌套,所以组件名称前必须加以限定,以确定是哪个容器的。其命名规则是:容器1… 容器n.组件类型.组件名称。这样还有一个优点,就是生成的测试脚本可读性强,易于用户进行修改。否则,像“在屏幕上某某位置点击鼠标”这种记录方式,用户根本无法理解其含义。而写成“动作:鼠标点击,对象:窗口mm|菜单nn, 点击位置:x,y”,则用户可以明了其动作语义,从而方便其修改测试脚本。
当回放用户输入事件时,还是通过直接操纵系统事件队列实现输入事件的回放。先通过记录下的窗口/组件的名称获得对应的窗口/组件的引用,然后结合记录的其它鼠标/键盘事件信息,重构鼠标/键盘输入事件。最后,将重构的鼠标/键盘事件直接放入系统事件队列中,由分派线程执行后续的事件分派工作。这样,就解决了由于窗口/组件的位置变动带来的回放定位问题。
这里需要解决的关键问题是如何能根据窗口/组件的名称获得其引用。这还是通过系统事件队列来实现的。因为Java程序在新建/删除一个容器时都会向系统事件队列发出一个Containerevent事件,其中包含了对该容器的引用。所以,事件回放器在载入被测程序后便监视系统事件队列,截获所有的ContainerEvent事件。如果是新建容器事件,便从中获得新建Container的 引用。因为所有的Container都实现了函数getComponents(),该函数返回容器包含的所有其它组件或容器的引用,通过一个简单的递归便可以得到一个容器包含的所有组件或容器。我们将窗口/组件的引用和其名称保存在Vector向量中,这样在回放过程中,只需知道窗口/组件的名称,便可从Vector向量中找到对应窗口/组件的引用。
3.4同步回放过程
(1)同步回放过程的必要性
一个设计良好的捕获/回放测试工具除了能捕获并回放用户事件外,更重要的是还必须能同步回放过程。因为,在回放过程中充满了各种变数会导致回放失败。同步回放过程的目的主要在于:1)决定何时回放下一个用户输入动作;2)对于回放失败,必须能尽早察觉;3)如果可能,应当在错误恢复后继续回放。
而现行的绝大多数捕获/回放工具,对回放事件的控制是依据捕获时输入事件之间的时间间隔。这种做法显然不够合理。因为,不同的机器运算速度(跨硬件平台测试)、运行环境(跨操作系统平台测试)对回放过程会造成影响,使得两次回放之间的时间间隔难以控制。即使在理想状况下可以不计这一因素,但还有一种情况是无法避免的,例如,测试网络程序,需要从Web 服务器下载Applet运行。此时,即使是在同一台机器上进行回放,由于网络速度的影响无法保证其延时。更有甚者,回放时由于服务器忙或关闭而联接失败。这样,本来后续动作是点击下载的Applet 的某个GUI组件,现在不论等待多久,回放的动作肯定错误。因此,传统的捕获/回放工具已经无法适应网络环境下的软件测试。
(2)基于事件跟踪的回放同步我们从以下4个方面来同步回放过程:1)根据记录下的连续两个输入事件间的时间间隔,但这只是回放的必要条件。2)因为连续两个输入事件间可能还有其它系统事件,例如,点击菜单后弹出一个窗体,就会产生FocusEvent、 WindowsEvent等系统事件。在捕获输入事件时,记录下连续两个输入事件间的其它系统事件。在回放一个输入事件时,必须保证在该输入事件前发生的其它系统事件都已按捕获时的顺序出现过。这样,还可以从某一方面发现回放错误,例如某个窗体/容器没有出现。3)在回放某一个输入事件时必须保证前一个输入事件已经被事件源处理完毕。因为事件分派线程对于系统事件队列中的事件是一个一个顺序处理的,前一个事件处理完了才分派系统事件队列中的下一个事件。但是,不能采用监视系统事件队列为空的办法来判断前一个事件是否处理结束。因为,一方面这样占用大量CPU时间,另一方面,系统事件队列为空并不表示前一事件已处理结束,可能还正在处理中。
我们的方法是:事件回放器每放一个输入事件到系统事件队列,就随之放入一个自定义的事件,该事件源是事件回放器。这样,当事件分派线程处理完输入事件后,就会将自定义的事件分派给事件回放器,从而事件回放器知道前一个输入事件已处理结束,如果此时又满足前两个条件,就继续回放下一个输入事件[6]。4)在回放输入事件时,必须保证该事件的事件源存在。
4结束语
随着我国软件业的发展,作为软件质量保证的重要组成的软件测试已越来越受到重视,而软件测试自动化工具由于可以提高软件测试的有效性和效率,因此,它的开发已成为软件测试的一个重要研究领域。本文在已经实现了的面向对象语言的软件测试自动化工具基础上,介绍了软件测试自动化工具中最常用也最有用的捕获/回放功能的实现原理。
参考文献
1 International Software Automation Inc..Panorama-2 C/C++User'sManual.International Software Automation Inc.,1993
2 Wang Sen.Inside the Java.Wuhan:Huazhong Science andTechnology University Press,2002
3 Horstmann C S,Cornell G.Java 2 Volume I:Fundamentals.Beijing:China Machine Press,2002
4 Palmer G.Java Event Handling.Beijing:Tsinghua University Press,2002
5 Adair D.How to Make an Event Log.Java Developer Connection,2002
6 Harper S.Using Java.awt.Robot to Measure Performance,Capacity,and Reliability.Sun's 2002 Worldwide Java Developer nference,2002