UML软件工程组织

 

 

使用 Abbot 框架自动化测试 Eclipse 插件的用户界面,第 2 部分
 
赵 勇 (zhaoyong@cn.ibm.com), IBM 中国SOA 设计中心工程师, IBM 中国软件开发实验室 SOA设计中心
 
 如今 Eclipse RCP 平台已成为 Java 平台上的富客户端首选,而 SWT 和 JFace 的高效率也让诸多 Java 界面开发者受益匪浅。在插件化已经成为一种潮流的今天,我们迫切需要一种自动化的界面测试工具去测试 Eclipse 的插件。Abbot 框架就是这样一种自动化的UI测试工具,它提供了一系列的能够执行 Swing 或者 SWT 界面测试的 API,并提供了脚本录制和编辑、运行的工具。Abbot 的 SWT 版本是基于 Eclipse 的插件形式发布的,天然的支持了 Eclipse 插件的自动化测试。

本文详细的描述了 Abbot SWT 插件的配置和使用,分析了 Abbot 的体系结构和工作原理,并给出复杂的测试用例来说明 Abbot SWT 的一些高级用法,同时还分享了作者的一些 Abbot 相关的最佳实践,相信会对从事 Eclipse 插件和 SWT 用户界面的开发和测试人员有一定的帮助。

循序渐进:编写高效的 Abbot 测试用例

至此我们已经能够基本掌握 Abbot 的用法,编写简单的测试用例,你也许没有注意到,我们的测试用例是否是有效的测试用例?我们是否可以用更简单的方式,编写更加高效的测试用例?在本文的这一部分,你将能够了解到这些内容。

使用自定义线程捕获异常

如果你使用JUnit测试过多线程程序,你会发现JUnit实际上不支持多线程的测试,所有在线程中的断言失败或者异常对该测试用例的结果没有影响。前面我们已经了解到,Abbot的测试通常是使用线程方式进行的,我们还沉浸在一开始就运行成功Java类向导测试用例的喜悦中,也许并没有发现,那并不是一个有效的测试用例。想知道事情的原委,读者可以将JavaWizardTest中findAndTestWizard方法的“assertFalse(finishButton.isEnabled()); ”更改为“assertFalse(!finishButton.isEnabled());”再次运行该用例,你将发现控制台中,JUnit捕获到了junit.framework.AssertionFailedError错误,但是用户界面却停滞不前,当你按下取消按钮帮助界面结束测试,你可以在JUnit视图中发现测试用例居然成功通过。

此时我们发现两个问题:

我们在线程中的断言和异常并不能真正的决定测试用例的成败,因此我们的测试用例是无效的。

 在发生异常后,我们并没有考虑到如何使用户界面继续(我们需要手工帮助向导退出),后续的测试方法和测试用例将无法继续进行。

 实际项目中,我们通常使用自定义的测试线程和增加在finally中对用户界面的清理工作来解决上面两个问题,首先我们定义一个能够简单地显示运行是否成功的线程,请参见清单10。

清单10:测试线程TestThread

package abbottest.sample;

public class TestThread extends Thread {
private Throwable exp = null;

public boolean isSuccess() {
return (exp == null);
}

public Throwable getExp() {
return exp;
}

public void setExp(Throwable exp) {
this.exp = exp;
}
}

该测试线程能够记录在运行过程中发现的错误,并提供方法来检查在运行是否成功完成。如果我们将Abbot的测试代码封装在这样的线程中运行,就可以在主线程中判断测试线程运行是否有错误,从而实现测试的目的。

这样我们经过修改的代码,置于JavaWizardTest2.java,清单 11显示了使用测试线程的JavaWizardTest2的代码。

清单11:使用测试线程的测试用例

package abbottest.sample;
import junit.framework.TestCase;

import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Widget;

import abbot.finder.matchers.swt.ClassMultiMatcher;
import abbot.finder.matchers.swt.TextMatcher;
import abbot.finder.matchers.swt.TextMultiMatcher;
import abbot.finder.swt.BasicFinder;
import abbot.finder.swt.TestHierarchy;
import abbot.tester.swt.MenuItemTester;
import abbot.tester.swt.Robot;
import abbot.tester.swt.TextTester;
import abbot.tester.swt.WidgetTester;

public class JavaWizardTest2 extends TestCase {
public void testJavaWizard() {
final TestThread wizThread = new TestThread() {
public void run() {
WidgetTester.waitForShellShowing("New Java Class");
try {
findAndTestWizard();
} catch (Throwable e) {
e.printStackTrace();
setExp(e);
}
}
};
wizThread.start();
assertTrue(wizThread.isSuccess());

}

private void openJavaWizard() {
MenuItemTester menuItemTester =
(MenuItemTester) WidgetTester.getTester(MenuItem.class);
menuItemTester.actionSelectMenuItem("&File/&New\tAlt+Shift+N/Class",
null, Display.getCurrent().getActiveShell(), 1000);
}

private void findAndTestWizard() throws Throwable{
TestHierarchy hierarchy = new TestHierarchy(Display.getCurrent());
final BasicFinder finder = new BasicFinder(hierarchy);
Button cancelButton=null;
try {
final Widget root = finder.find(new TextMatcher("New Java Class"));
final Text nameText = (Text) finder.find(new ClassMultiMatcher(Text.class, 4));
final Button finishButton=(Button)finder.find(root,
new TextMultiMatcher("&Finish",1,Button.class));
cancelButton=(Button)finder.find(root,new TextMultiMatcher("Cancel",1,Button.class));

abbot.tester.swt.Robot.syncExec(root.getDisplay(), null, new Runnable() {
public void run() {

TextTester textTester = new TextTester();
textTester.actionEnterText(nameText, "classname");

Robot.delay(1000);
assertFalse(!finishButton.isEnabled());

}
});
} catch (Exception e) {
e.printStackTrace();
throw(e);
}finally
{
WidgetTester.getWidgetTester().actionClick(cancelButton);
}
}
}

以上代码展示了新的测试线程的用法,并且我们在finnaly中退出该向导,可以保证该测试用例发生错误以后其余的测试用例能够继续运行。同时我们捕获的是Throwable,可以保证捕获到Abbot的异常和JUnit的断言失败。

至此一切无懈可击,此时你可以运行新的测试用例,等等,为什么运行以后虽然有异常,测试用例看起来还是成功的呢?请在 openJavaWizard();和assertTrue(wizThread.isSuccess());之间 加入清单12中的代码。

清单12:在测试方法中增加的代码。

while (wizThread.isAlive()) {
WidgetTester.getWidgetTester().actionDelay(100);
}

再次运行,终于成功通过测试,为什么会这样呢?这是因为测试线程的执行还没有完全完成,但是测试方法中的代码已经走到断言部分了,增加了以上的代码,可以保证在UI线程结束后在继续运行当前的测试方法内的代码,这就能保证测试方法内所有的代码(包括线程内的)都能够同步的被执行,有兴趣的读者可以研究一下Abbot的源码,了解详细的情况。如果你需要在一个测试方法中进行多个在新线程中顺序执行的界面测试行为,这种同步显得尤为重要,你都要使用以上的方法保证这些线程在当前线程的控制之下按顺序的执行,同时需要注意代码的写法,以保证所有的错误能够被当前线程捕捉到,真实地反映到当前的测试用例中。

基于以上方法,我们可以编写真正高效的测试用例,真正捕获到任何异常和错误,但是我们还有方法使你的测试用例更为简单。

使用 abbot.swt.eclipse 简化你的插件测试

abbot.swt在abbot的基础上增加了很多SWT的支持,类似的是,Abbot还提供了abbot.swt.eclipse插件为我们的Eclipse插件测试提供了一些便利的功能。abbot.swt.eclipse插件则在abbbot.swt插件的基础上增加了一些实用的测试方法和测试器(图1),极大的方便了对基于SWT的Eclipse插件用户界面的测试。

图1:新增的测试器
 
 上图可以看出,abbot.swt.eclipse插件提供了Abbot SWT插件所没有的对话框测试器,能够方便的执行对话框的测试。同时在“utils”包中还提供了很多的实用类,见图2,

图2,abbot.swt.eclipse的utils包
 
 这些类的作用一目了然,的确可以帮助我们方便的进行插件的测试,例如,使用类InvokeNewWizard,我们可以很方便的打开新建Java类向导,只要一行语句就可以实现,见表清单13。

清单13:使用InvokeNewWizard打开新建Java类向导

InvokeNewWizard.invoke("Java/Class", Display.getCurrent().getActiveShell());

Abbot 还提供了更好的Abbot TestCase,内置一些测试器以及一些Shell之类Eclipse特有的对象,我们的测试用例可以从“abbot.swt.eclipse.tests.TestCase”派生,可以节省一些工作量。同时在“abbot.swt.eclipse.tests”包中还有很多的测试用例,读者可以学习一下,具有很大的参考价值。

总体上来说,使用abbot.swt.eclipse插件可以方便的进行Eclipse插件的用户界面测试,这里我们在使用方便之余也能体会到Eclipse插件编程思想中的插件分层思想。分层的思想,对于我们编程序和做项目具有很重要的意义,Eclipse的插件编程,严格的将界面、模型、核心、业务逻辑实用工具等分隔在不同的插件中实现,既能简化插件的开发,又能保证功能的简化。比如Abbot中的三个插件,abbot,abbot.swt,abbot.swt.eclipse,三者之间分工明确,协同工作就能完成Eclipse插件的用户界面测试,也可以单独使用。这里需要说明的是,abbot.swt.eclipse这个插件现在还没有太多的功能,不过我们可以预见,一定会为Eclipse插件的测试提供强大的支持。

更上层楼:构建复杂的插件测试用例

目前为止我们已经能够了解到Abbot SWT的全貌,也掌握了Abbot的一些高级用法,现在可以通过一个复杂的测试用例来看看如何在Eclipse环境中使用Abbot开发有效的用户界面测试用例。我们将使用Eclipse自带的一个插件示例作为被测试的插件,我们的测试用例就是要测试该插件的用户界面行为。

创建示例插件

我们将使用Eclipse的插件模板创建一个简单的示例插件,首先新建一个名为SampleView的插件项目,在向导的最后一页选择“Plug-in with a view”,点击“Finish”。该示例的目的是供使用者学习视图的编程和相关的操作,观察SampleView.java,我们发现很多的字符串变量,前面提到Abbot的搜索器和匹配器很大程度上依赖于字符串的匹配,为了便于测试,我们需要一定的重构,主要是字符串放到Message中,然后在测试用例中引用,重构后的代码请见附件。此时你可以启动Eclipse的Runtime,打开“Show View” 对话框,如图 3 所示。

 图3:选择“Sample View”
 
 选中“Sample Category”下的“Sample View”来打开该插件的视图,点击确认打开Sample View,该视图是一个简单示例,包含一个简单的Table,如图4。

图4:Sample View
 
 双击table中的item会弹出对话框,在树节点上选择不同的右键菜也会弹出不同内容的对话框。这个就是这个示例插件的主要逻辑,对于初学者,这是一个很好的学习对象。对于我们,则可以用来作为一个待测试的插件,我们将使用Abbot构建一些端到端的测试用例,来测试该视图的功能。为了测试,你还需要添加给AbbotTest 插件添加新的Dependency,也就是SampleView插件。同时,请在SampleView插件的Runtime设置页中将如图的 2 个包作为导出的包,能够被测试插件引用到,如图5。

图5;导出sampleview的包
 
 使用 Abbot 构建复杂的测试用例

通过对被测试对象的简单分析以后,我们需要创建两个测试方法,分别为测试视图中的双击事件和弹出菜单,同时需要一个方法能够打开视图。我们创建一个名为SampleViewTest的测试用例,详细代码请参考附件中的源码,下面分别介绍该测试用例中三个主要的方法。

显示视图

一旦我们运行 JUnit Plug-in Test ,JUnit 会启动一个新的 Eclipse Runtime,我们需要打开待测试的视图,因此清单 14 中的 openSmapleView 方法向我们展示了如何使用 Abbot API 打开一个视图。

清单14:SampleViewTest的openSmapleView方法

protected void openSmapleView() {

final Thread showViewThread= new Thread() {
public void run() {
WidgetTester.waitForFrameShowing("Show View");
TestHierarchy hierarchy = new TestHierarchy(Display.getCurrent());
final BasicFinder finder = new BasicFinder(hierarchy);

try {
final Widget root = finder.find(new TextMatcher("Show View"));
final Text nameText = (Text) finder.find(new ClassMultiMatcher(Text.class, 1));
abbot.tester.swt.Robot.syncExec(root.getDisplay(), null, new Runnable() {
public void run() {
TextTester textTester = new TextTester();
textTester.actionEnterText(nameText,
Messages.SampleView_MSG_DLG_Title);
Robot.delay(1000);
wt.actionKeyPress(SWT.CR,Display.getCurrent());
Robot.delay(1000);
wt.actionKeyPress(SWT.CR,Display.getCurrent());
}
});

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

showViewThread.start();

wt.actionKey(SWT.ALT+
SWT.SHIFT+
(new Integer('q')).intValue(), Display.getCurrent());
Robot.delay(1000);
wt.actionKey( (new Integer('q')).intValue(), Display.getCurrent());

Robot.wait( new Condition(){
public boolean test() {
return isSampleViewOpened();
}});
}

注意我们用到了上文提到的快捷键的方式来激活显示视图对话框,并使用键盘操作来避免复杂的树操作,注意构件测试器的actionKey方法的用法,通过该方法,你可以方便的进行UI操作,而避免复杂的鼠标操作,对于测试鼠标操作无关的UI,使用快捷键操作可以避免书写冗余和重复的代码。

试着注释线程内的 delay 语句,你将发现,在树节点还未被选中之前按钮动作就被触发,所以界面无法继续下去。因此在使用Abbot时,我们需要树立一个观念,用户界面的打开或者显示绝对不是一瞬间的事情,我们经常会用等待方法保证用户界面在Abbot的指引下顺利的运行。

最后的Robot.wait方法向我们展示了自定义等待条件的用法,我们需要调用该方法确定视图已经被打开(因为视图的打开同样需要时间,如果不等视图完全显示就进行下一次操作,可能会导致错误)。在Abbot测试用例中你会经常使用该方法去等待业务操作的完成,实现一个简单的Condition接口就可以做到。

测试弹出菜单

使用打开视图方法可以打开示例视图,一旦确认示例视图被打开,我们就可以开始测试,清单15展示了弹出菜单的测试方法。

清单15:弹出菜单的测试方法。

public void testPopupMenu()
{
if(isSampleViewOpened()==false)
openSmapleView();
TestHierarchy hierarchy = new TestHierarchy(Display.getCurrent());
final BasicFinder finder = new BasicFinder(hierarchy);
try {
final TableItem item =(TableItem)finder.find(
new TextMultiMatcher(Messages.SampleView_One,1,TableItem.class));
final Menu m = item.getParent().getMenu();
Thread menuClick = new Thread() {
public void run() {
item.getDisplay().syncExec(new Runnable() {
public void run() {
new Robot().keyPress(KeyEvent.VK_ESCAPE);
}
});
}
};
menuClick.start();

// Right mouse click on TreeItem
itemTester.actionClick(item, 1, 1, "BUTTON3");

MenuItem[] aItems = new MenuTester().getItems(m);

Thread actionDialog = new Thread() {
public void run() {
WidgetTester.waitForFrameShowing("Sample View");
try {
final Widget root = finder.find(new TextMatcher("Sample View"));
Label label=(Label)finder.find(root,
new TextMultiMatcher(Messages.SampleView_Action2_executed,
1,Label.class));
assertNotNull(label);
Robot.delay(1000);
wt.actionKeyPress(SWT.CR, root.getDisplay());

} catch (Exception e) {
e.printStackTrace();
fail(e.getMessage());
}
}
};
actionDialog.start();

itemTester.actionSelectPopupMenuItem(aItems[1], 10, 10);

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

}
private boolean isSampleViewOpened() {
IWorkbenchPage activePage =
PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
if (activePage.getActivePart() instanceof SampleView)
return true;
return false;
}

这里我们使用到了激活鼠标右键的操作:itemTester.actionClick(item, 1, 1, "BUTTON3"),该句语句中"BUTTON3"表示右键,这条语句能够执行右键单击的操作,从而显示右键菜单,类似的操作我们在Abbot的测试中会经常使用到。这里有个奇怪的menuClick线程,该线程的作用是点击键盘上的退出键,读者可以尝试注释该线程的代码,运行测试,观察用户界面的显示,你可以发现界面被阻塞在右键菜单处,无法选中菜单。在显示菜单后,执行menuClick线程就可以避免这种情况的出现。

读者也可以看见,我们会通过root参数缩小查找范围,这里的root就是弹出对话框的Shell。在“wt.actionKeyPress(SWT.CR, root.getDisplay());”语句中你同样需要使用该shell对象,该语句的意思是在弹出的对话框中按下回车键,显然比找到OK按钮,然后点击OK按钮要方便的多。同时因为测试线程只是进行了简单的判空操作,并没有直接执行UI构件的操作,所以我们不需要在使用Robot.syncExec方法执行测试代码,而是将其直接置于线程体内即可。在编写具体的测试用例的时候,我们应当尽量以简单易懂为原则,灵活的使用Abbot API,编写出简洁高效的测试用例。

测试双击事件

测试完右键菜单后,我们再看看如何测试表中的Item的鼠标双击事件,参见清单16的代码。

清单16:测试鼠标双击的方法

public void testDoubleClick()
{
if(isSampleViewOpened()==false)
openSmapleView();
TestHierarchy hierarchy = new TestHierarchy(Display.getCurrent());
final BasicFinder finder = new BasicFinder(hierarchy);

Thread actionDialog = new Thread() {
public void run() {
try {
final TableItem item =(TableItem)finder.find(
new TextMultiMatcher(Messages.SampleView_One,1,TableItem.class));
assertNotNull(item);
wt.actionClick(item, 6, 3, "BUTTON1", 2);
WidgetTester.waitForShellShowing(Messages.SampleView_MSG_DLG_Title,2000);

final Widget root = finder.find(
new TextMatcher(Messages.SampleView_MSG_DLG_Title));
Label label=(Label)finder.find(new TextMultiMatcher(
NLS.bind(Messages.SampleView_Double_click,
Messages.SampleView_One),1,Label.class));
assertNotNull(label);
Robot.delay(1000);
wt.actionKeyPress(SWT.CR, root.getDisplay());
} catch (Exception e) {
e.printStackTrace();
fail(e.getMessage());
}
}
};
actionDialog.start();

// safe join
while (actionDialog.isAlive()) {
wt.actionDelay(100);
}

}

双击事件的测试再次向我们展示了鼠标事件的用法,通过“wt.actionClick(item, 1, 1, "BUTTON1", 2)”这行代码,我们可以单击鼠标左键两次,注意如果不加以控制,这两次的点击会引起线程相关的问题,所以我们使用上文论述过的方法来控制代码能够同步执行。也就是我们首先启动测试线程,然后使用while方法等待,测试线程不执行完毕,当前线程就一直等待,这样可以保证的测试线程内的测试动作完全执行完成后再将控制权返回当前线程,继续运行。

本部分的代码展示一个复杂的测试用例中怎样进行一些复杂的测试,完整的测试代码请参见附件。读者也许已经意识到,编写复杂的Abbot测试用例会强迫测试人员对SWT用户界面的运行原理、事件、线程等机制有比较深入的了解,这也从另外一个方面促使你更加深刻的掌握SWT的用户界面编程。因此测试用例对我们的影响是多方面的,除了上文论及的国际化和易访问性,也会对我们的知识的深度进行一次洗礼,使你对被测试的代码和背后的运行原理,有更为深刻的认识。

总结

至此,我们已经能够了解 Abbot 的基本概念和原理,并能理解和执行一些测试用例。我们可以看出 Abbot 整个的逻辑非常的简单,无非是找到一些待测试的构件,执行相关的动作,比较测试的结果。如果组合使用这些简单的逻辑,就能够测试很复杂的界面。对于用户定制的特定的UI,比如使用 gc 直接绘制的构件,或者是一些特定的复合构件可能需要对 Abbot 做相应的扩展,你也可以分析 Abbot 的源代码,扩展相应的测试器来解决这些问题。在 Abbot 的测试中尤其需要注意线程同步的问题,文中我们很多地方都有讨论,读者在自己编写测试用例的时候需要注意。虽然编写端到端的用户界面测试用例比较麻烦,但是熟悉以后,你可以很快的写出高效的测试用例,显著提高你的开发和测试效率。

本文并没有讨论到Abbot的脚本和Swing的Abbot测试。各种各样的脚本在我们的编程中总是起着事半功倍的效果,通常我们使用的各种测试工具都会有脚本的概念,Abbot 也不例外。Abbot使用可以简单的xml文件记录测试过程,并提供了脚本编辑器来编辑和运行,有兴趣的读者可以从Abbot站点获取相关的资料。关于AWT和Swing的测试,Abbot的网站上提供了足够的示例,如果需要的读者可以访问Abbot站点。

从实用的角度来说Abbot插件的功能已经足够应付一般的Swing和SWT的测试,不过其针对Eclipse插件的测试相关的功能还在开发过程中,我们希望尽快能够有发布的版本,能够更全面的支持Eclipse插件的测试。

就像你为所有重要的功能编写了单元测试一样,我们建议你为重要的界面编写测试用例,尤其对于一些数据敏感的动态生成的用户界面,Abbot的测试非常重要。但是,我们不推荐像写单元测试一样写细粒度的用户界面测试,自动化的工具不能完全代替人去执行手工的测试。即使我们有100%覆盖的用户界面测试用例,也不能完全保证用户界面适用于真正的用户,何况,编写100%覆盖的用户界面测试用例是不可能的,UI的外观,易用性,单词的拼写错误,构件的对齐等很多问题,只能由人去测试。但是Abbot等自动化测试工具的意义仍然非常重要,如同我们展示的一样,对于重复的界面测试,数据或者业务逻辑紧密联系的界面,界面的流程和操作等,使用工具或者脚本能够极大的减少测试人员的工作量。实际项目中,我们更倾向于通过UI的动作来测试业务逻辑,通过端到端的测试用例来实现功能的测试,除了自动化程度高以外,还可以避免代码的改变和业务逻辑的改变带来的测试用例的改变。对于一个设计良好的Eclipse插件,UI的变化频率应该远远的低于业务逻辑的变化(在某种的意义上),因此通过界面去测试业务逻辑是很好的一种选择。选择重要的流程,通过Abbot构建端到端的测试用例,能够覆盖到绝大多数的业务流程,这样的测试经过我们实践的证明是有效的。

参考资料

从 Abbot SourceForge 主页上获取大量的示例和教程。

访问 Abbot CVS 仓库配置,获取最新版本软件。

关于作者

赵勇,IBM 中国软件开发实验室 IBM 中国 SOA 设计中心工程师。具有多年的 J2EE 和 Web Service 开发经验,目前专注于 SOA 项目实践和相关的理论,工具的研究和开发。对 ESB、SCA、BPEL、自动化测试和极限编程等技术有浓厚的兴趣。联系方式:zhaoyong@cn.ibm.com。



 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号