UML软件工程组织

生成“伪视频”来丰富自动测试的日志系统
章 国俊 (zhanggj@cn.ibm.com), 软件工程师, IBM中国软件开发中心

本文内容包括:

一、 生成“伪视频”来丰富自动测试的日志系统
 二、 日志系统的改进
 三、 改进方案描述
 四、 改进方案的实现
 五、总结
 参考资料
 关于作者

IBM Rational Functional Tester是由IBM推出的针对Java,.Net和Web应用程序的自动化测试工具,借助这一工具,测试人员可以轻松地录制或编写脚本来进行自动化测试,测试效率得到显著提高,因而受到广大功能测试人员的青睐。由于自动测试的运行无需人工干预,日志系统作为记录运行过程的载体,对于测试成功与否的判定、错误的跟踪与分析都有着非常重要的作用,是自动测试框架中不可或缺的组成部分。

 一、 生成“伪视频”来丰富自动测试的日志系统

GUI自动测试的必要性

GUI 即 Graphical User Interface, 图形用户界面;GUI测试,顾名思义,是对图形用户界面进行测试。由于人机交互界面对于大部分应用软件产品都是必不可少的,而且软件的显示和功能也基本上是通过人机交互操作来体现和完成的,所以软件产品的GUI测试在整个产品测试中占有非常重要的地位。

GUI测试我们可以采用手工和自动化的测试方法。对于一个带有较多功能和较复杂界面的软件产品,单调繁琐的回归测试会让手工测试人员而感到十分枯燥和疲惫,进而影响了测试的准确度和效率;相反,使用自动化方式进行功能测试则会使情况大为改观:机器在进行步骤简单、较少互动的测试操作时,无论是效率还是准确度,都比手工测试更加优越。随着自动化测试功能的延伸和适应性的增强,它在GUI测试中所覆盖的工作范围,也在日趋扩大。

基于Rational Functional Tester构建自动测试框架

IBM Rational Functional Tester是由IBM推出的针对Java,.Net和Web应用程序的自动化测试工具,拥有功能强大的编辑器并支持多种脚本语言,还集成了ScriptAssure 技术、模式匹配、及数据驱动等高级特性,以增强测试脚本的灵活性。借助这一工具,测试人员可以轻松地录制或编写脚本来进行自动化测试,测试效率得到显著提高,因而受到广大功能测试人员的青睐。

Rational Functional Tester作为一款通用的功能测试工具,除了提供基本功能外,同时向广大使用者开放了一组API工具包供扩展使用(可参阅IBM Rational Functional Tester的联机帮助文档)。针对具体的被测应用软件,测试人员可以拓展、包装RFT的API,或引入其它工具,以实现必要的辅助功能。通常情况下,为了更好地执行GUI自动测试,一个基本的测试框架还需要加入以下辅助功能:测试数据的导入、测试环境的定制、测试执行日志、测试结果的生成和分析等等。

由于自动测试的运行无需人工干预,日志系统作为记录运行过程的载体,对于测试成功与否的判定、错误的跟踪与分析都有着非常重要的作用,是自动测试框架中不可或缺的组成部分。

二、 日志系统的改进

传统日志方案介绍

我们常见的日志方案简单说来就是:“出错后截图+文本日志”。随着自动测试的运行,模拟生成的各种鼠标、键盘操作也会被记录在文本日志文件中,标明鼠标点击了某某按钮、在某文本框内输入了某些文字等等。当用户界面的表现与预期情况不一致时(通常预示着该测试用例失败,但也有可能是由于测试脚本的不完善所致),测试框架会抓下当前屏幕的内容,以图片形式保存,作为检查和回溯时的依据。

文本日志和出错后的截图可以基本反映自动测试的过程;通过截图,测试人员还可以了解到出错时的软件界面场景,对错误进行分析和纠正。如果是软件缺陷导致的,表示测试用例失败,需要及时撰写测试报告,向开发人员反馈测试结果;如果是由于测试脚本的不完善所致,测试人员还需要对自动测试脚本进行修正,避免在日后的自动测试过程中,产生不必要的错误。

传统日志方案的不足

但传统日志方案也有其明显的不足之处,主要体现在:

  1. 信息有限;
    只有简单的文本和截图。
  2. 学习曲线陡峭,可用性差;
    利用有限的文本日志和少量截图,测试人员需要结合以往经验来推测错误成因,可靠性不高,而且非常吃力;新手则需要经过很长时间的实践积累才能具备这一技能。
  3. 难以发现部分隐藏的错误原因;
    部分错误往往是由一段时间前的操作导致,原因隐藏得较深,有一个积累的过程;出错后的截图并不能反映过程相关的信息。

    改进思路

针对传统日志方案的不足之处,我们可以有的放矢,根据它的不足提出改进方案。主要思路有以下两点:

  1. 体现错误发生的过程,而不仅仅是结果;
  2. 以更简单的方式来表达出更多的信息;

三、 改进方案描述

在改进前的日志方案里,自动测试软件是按照既定逻辑运行测试用例,无论是通过录制还是编写脚本的方式;遇到错误后,它会截取当前屏幕的状态,同时将错误信息以文本方式记录在日志文件中。(如图1所示)

该方案向测试人员提供了一副描述错误现场的截图,以及文本形式的执行日志。为了找出导致错误发生的确切原因,测试人员需要追踪并分析执行日志,截图反映了发生错误当时的情形,但对于之前过程的反映,却相当有限。

借助错误现场的截图和文本日志,测试人员判断测试失败的症结所在会非常吃力,尤其是一些不太明显的错误。即便是富有经验的测试人员,自动测试的错误分析也是比较棘手的工作,新人则更是无从下手。

图一: 传统日志方案
 
 在改进后的日志方案里,我们设计了一种更为简单和直观的方式,来反映错误发生前后的那段时间内,自动测试的真实运行过程。(如图二 所示)

图二: 改进后的日志方案
 
 改进措施如下:

  • 按固定时间间隔截屏;并建立缓冲区,存储最近一段时间内的截图;
    该缓冲区是一个先入先出队列,只存储最近一段时间内的截图,随着自动测试的运行,框架会按照固定时间间隔不断地截屏,存入缓冲区。缓冲区可以有两种实现方式:指定逻辑长度,用来储存某个测试用例执行时的所有截图;或者是指定物理长度,只存储一定数目的截图;
  • 一旦在自动测试运行过程中发生错误,所有被缓冲的截图都被收集起来,并转化为动态图片(GIF格式,PNG格式,SVG格式等等);
    动态图片按时间顺序展示了运行过程中若干副截图,效果类似于视频片断,真实地再现了当时的测试过程。鉴于测试过程中并非每时每刻都是关键操作,以及测试软件对测试对象的定位也会占用一定的时间,所以并没有必要以过高的频率截屏,通常来说,每秒1帧或2帧已经可以满足再现过程的需求,我们将它称为“伪视频”片断。
    新方案在已有功能的基础上,进行重构和转化,模拟生成视频片断来再现测试过程。(对测试运行过程的屏幕截图进行缓冲。必要时,将所有缓冲的图片转化成动态图片,以再现实际过程。)

新方案的优点

日志系统在采纳新方案后,可以:

  1. 使得自动测试中的错误定位更加快速,有利于测试人员修改测试脚本或汇报被测软件的缺陷;
  2. 让日志系统更加直观,直观丰富的日志信息让自动测试系统的门槛降低,提高了它的可用性;
  3. 无需追加投资;新方案是以一个新思路改进原有的截屏功能,使其呈现出更丰富更灵活的信息,并未添加软硬件来进行视频捕捉。新方案的实现几乎没有成本,任何具有截屏能力的自动测试系统都可以在简单改造后拥有该特性。

截屏以及生成伪视频的操作会对脚本回放速度有一定影响,但并不突出,因为自动测试有着充足的硬件资源和时间资源(夜间运行)。这一弱点和新特性带来的好处相比,几乎可以忽略不计。

四、 改进方案的实现

假设该自动测试框架的原日志方案已经具有生成文本日志和截图的功能,我们将在它的基础上进行改进和优化,使之具有生成伪视频日志的新特性。

图三阐述了改进方案的基本工作步骤:

  1. 测试人员启动某自动测试脚本script1的运行;
  2. 自动测试脚本script1进行初始化工作,并创建缓冲区screenshotsPool来储存测试过程中捕获的截图;
  3. 自动测试脚本script1按既定逻辑顺序执行;
  4. 与此同时,缓冲区screenshotsPool正独立运行,以固定频率不断地截取屏幕图像;
  5. 如果缓冲区到达最大容量,较早的截图会在存入新图前被删去;
  6. 一旦自动测试发生错误,script1会通知screenshotsPool有错误产生;而后screenshotsPool搜集所有缓冲截图,立即生成动态图片(伪视频);
  7. Script1的自动测试结束;

图三: 改进方案的时序图

 本例中给出的代码主体ScreenshotsPool将以线程的形式运行,独立于自动测试脚本的回放,因此实现了Runnable接口中的run()方法。(见图四)

此外,还包括了其它必要的属性,方法来完成截图、储存、生成伪视频等操作,以及相关设置。后面会做详细介绍。

在每个RFT脚本的初始化方法中,可以同时创建一个ScreenshotsPool对象,用以对脚本自动回放过程进行监控。

 图四: 改进方案的类图

 新方案关键代码解释

清单一: 实现代码中的主要属性

//单件模式,用来储存截图的缓冲区。
private static Vector bufferVec = null;
//标识缓冲区是否储存固定数目的截图,默认为固定数目。
private boolean isLimitedBuffer = true;
//伪视频所覆盖的自动测试过程,单位为秒;默认情况下存储最近30秒内的截图。
private int videoLength = 30;
//截图的时间间隔,单位为秒;默认情况下每隔1秒执行一次抓屏操作。
private double snapInterval = 1;
//缓冲区容量,该属性值由videoLength和snapInterval共同决定。
private int bufferCapability;
//伪视频输出位置
private String outputLocation = "c:/";
//伪视频格式,即生成的动态图片格式,在本例给出的实现使用了GIF格式。
private String pseudoVideoFormat = ".gif";
//标识生成的伪视频是否循环播放,默认为只播放一次。
private boolean replayIndefinitely = false;
//伪视频的重放速度,可选值为1,2,3。分别表示快速、中速和慢速。
private int replaySpeed = 2;
//标识是否有错误发生。当其值为true时,表明需要立即生成伪视频。
public static boolean errorFlag = false;

清单二: 实现代码中的主要方法

/**
* Create a screenshot pool with limited capability, it only
* stores enough screenshots to create a pseudo-video of appointed length
*
* @param videoLength - length of pseudo-video
* @param snapInterval - interval to scratch screenshots, the unit is second
* @param replayIndefinitely - the video plays once or repeats indefinitely
* @param replaySpeed - play speed of the video,1 means fast,2 is normal,and 3 means slow
*/
public ScreenshotsPool(int videoLength, double snapInterval,
boolean replayIndefinitely, int replaySpeed) {
super();
this.isLimitedBuffer = true;
this.videoLength = videoLength;
this.snapInterval = snapInterval;
this.replayIndefinitely = replayIndefinitely;
//设置有效的回放速度;
if((replaySpeed==1)||(replaySpeed==2)||(replaySpeed==3))
this.replaySpeed = replaySpeed;
//通过伪视频长度videoLength和抓屏频率snapInterval计算出缓冲区的容量
this.bufferCapability = new Double(videoLength/snapInterval).intValue();
//初始化缓冲区
getBufferVec();
}

ScreenshotsPool方法为构造器,按照给定的参数对必要的属性进行初始化,并初始化唯一的缓冲区。

/**
* Get buffer pool that store screenshots
*
* @return Vector - the only buffer pool to store screenshots
*/
public Vector getBufferVec()
{
if(null==bufferVec)
bufferVec = new Vector(bufferCapability);
return bufferVec;
}

getBufferVec()方法是单件模式,用来获得唯一的截图缓冲区;如果缓冲区暂时还不存在,则进行初始化。

/**
* Push a new screenshot into buffer
* if the buffer is limited and has reached its capability, the oldest
* snapshot would be removed
*/
public void pushScreenshot()
{
//获得当前屏幕的截图对象
BufferedImage image = getCurrentScreen();
if(null==image)
return;
//如果截图缓冲区有固定容量,
if(isLimitedBuffer){
//如果已经到达缓冲区的最大容量,则删去第一副截图
int size = getBufferVec().size();
if(size==bufferCapability)
getBufferVec().remove(0);
}
//将当前截图存入缓冲区
getBufferVec().add(image);

}

pushScreenshot方法用来将当前屏幕的截图存入缓冲区内。如果截图数目到达缓冲区上限,最早的截图会被删除。

/**
* Get current screenshot
*
* @return BufferedImage - screenshot of current screen
*/
public BufferedImage getCurrentScreen()
{
try{
//获得屏幕大小
Dimension screenSize = toolkit.getDefaultToolkit().getScreenSize();
int width = screenSize.width;
int height = screenSize.height;
BufferedImage capture = null;
//获得当前屏幕范围
Rectangle area = new Rectangle(0, 0, width, height);
Robot robot = new Robot();
//截取当前屏幕范围内的内容,返回BufferedImage对象
capture = robot.createScreenCapture(area);
return capture;
}
catch (Exception e) {
e.printStackTrace();
return null;
}
}

getCurrentScreen方法用来获得当前屏幕的截图,返回值是一个包含当前屏幕图像的BufferedImage对象。

/**
* Generate pseudo-video with those buffered screens in pool
*
* Here we will generate animated picture of '.gif' format, and we can
* generate animated picture of other formats likewise.(SVG, PNG, etc.)
*/
public void generatePseudoVideo()
{
int size = this.getBufferVec().size();
if(size<1)
return;
try{
BufferedImage bi = null;
//新建一个AnimatedGifEncoder对象,用来生成动态的GIF文件
AnimatedGifEncoder pseudoVideoGenerator = new AnimatedGifEncoder();
//设置GIF文件是否循环显示各帧
int repeat = replayIndefinitely?0:1;
pseudoVideoGenerator.setRepeat(repeat);
//设置输出位置,以当前时间为文件名
pseudoVideoGenerator.start(outputLocation+getCurrentTime()+pseudoVideoFormat);
//依次将缓冲区内的所有截图按指定间隔加入AnimatedGifEncoder对象
for(int i=0;i<size;i++){
bi = (BufferedImage)(getBufferVec().get(i));
pseudoVideoGenerator.setDelay(200*replaySpeed);
pseudoVideoGenerator.addFrame(bi);
}
//生成动态的GIF文件
pseudoVideoGenerator.finish();

}catch(Exception e){
e.printStackTrace();
}

}

generatePseudoVideo方法将缓冲区内的截图集中起来,每副截图作为一帧加入到一个生成器,再对相关参数做必要的设置,最后导出到一个GIF文件中。

其中,AnimatedGifEncoder类是一段开源程序,用来将一帧或多帧图像转换为一个GIF文件。作者是Kevin Weiner。

/**
* main method of this thread
*/
public void run()
{
while(true)
{
try
{
//将当前屏幕的截图存入缓冲区
pushScreenshot();
//睡眠一段时间
Thread.sleep(new Double(1000*this.snapInterval).longValue());
//查看错误标识,一旦有错
if(errorFlag)
{
//立即生成伪视频
generatePseudoVideo();
//复位错误标识
errorFlag = false;
}
}
catch(Exception e)
{
e.printStackTrace();
}
}

}

run()方法是该线程的主要方法,通过轮询方式监测错误标识。如果在自动测试脚本在运行过程中有错误发生,它会将错误标识设为true;run()方法侦测发现该标识后,在第一时间内生成伪视频,再对错误标识进行复位。

五、总结

新的日志方案在原有方案的基础上进行了重构,对错误后截图的功能加以萃取和改进,使原有日志系统在不追加任何软硬件投资的前提下,能生成具有类似功能的伪视频。测试人员能够直观地了解自动测试过程,快速定位测试脚本或软件缺陷导致的错误成因,从而高效地优化测试脚本或填写测试报告。

本例给出的实现使用了GIF格式的动态图片作为生成的伪视频,读者可以按照类似方法,生成诸如SVG,PNG等其它格式的动态图片来再现测试过程。

对于新日志系统中的各种各样的表现形式:文本日志、截图、以及伪视频,可以使用HTML格式统一展现,有概况有明细,通过超链接将众多日志内容组织在一起,让日志系统能更加友好方便地供测试人员查阅。

参考资料

有关 Rational Functional Tester 的详细信息,请参阅 Rational Functional Tester 专题

通过参与 developerWorks blogs

封装Rational Functional Tester API 来执行公共操作,请参阅 将调用封装到 Rational Functional Tester API 中。

关于作者

章国俊,软件工程师,2004 年加入 IBM 中国软件开发中心。主要从事 IBM Workplace 及相关产品的测试工作。对测试技术、软件过程及软件质量控制有着浓厚的兴趣。您可以通过电子邮件zhanggj@cn.ibm.com与他联系。


版权所有:UML软件工程组织