您肯定遇到过测试脚本的执行结果有时成功有时失败。这也是在做自动化测试项目中困扰我们的一个问题。在长期的项目实践中我们发现脚本的不稳定性主要是由于脚本中代码对时间把控的不够好导致的。测试脚本和 AUT 是在两个不同的进程中运行,缺少同步机制。脚本中的代码在不恰当的时间点对测试对象进行操作很容易导致意想不到的结果。比如模式对话框关闭时,还没有完全消失,就对其父窗口进行操作,将导致异常发生。过快的文字输入,测试对象来不及接收,将导致测试数据不完整。本文将介绍一些实际项目中积累的经验,希望对 Rational Functional Tester 初学者有所帮助。
有些 GUI 对象会出现的特别慢。这种的情况下,可以用 TestObject.waitForExistence() 等待足够长的时间后再对其进行操作。例如代码清单 1 所示。
清单 1.
button().waitForExistence(60, 5); button().click(); |
相反有些 GUI 对象会消失很慢,过快的执行下一步操作导致脚本失败也是常见现象。RFT 并没有提供类似 waitForExistence 的方法来等待对象消失。可以通过代码清单 2 所示实现一个 waitForInexistence
。像代码清单 3 所示来使用它。
清单 2.
/** * 判断测试对象在指定时间内是否消失了 * @param testObject 测试对象 * @param timeout 最大等待时长 * @param interval 重新检测的时间间隔 * @return */ public boolean inexists(TestObject testObject, double timeout, double interval) { long startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime < timeout * 1000) { if (!testObject.exists()) return true; sleep(interval); } return !testObject.exists(); } /** * 等待测试对象消失。如果在指定时间内测试对象没有消失,将抛出异常 * @param testObject 测试对象 * @param timeout 最大等待时长 * @param interval 重新检测的时间间隔 */ public void waitForInexistence(TestObject testObject, double timeout, double interval) { if (!inexists(testObject, timeout, interval)) throw new RuntimeException("TestObject doesn't disappear!"); } |
清单 3.
// 关闭 foregroundDialog,但它需要做些清理工作,会消失的很慢。foregroundDialog().close(); // 我们需要等待一下。否则,下一步的 click 可能被 foregroudialog 挡住 waiForInexistence(foregroundDialog(), 10, 2); backgroundDialog().click(); |
很多情况下我们等待某种条件成立时,才可以进行一下操作。我们可以定义一个 Condition 类,在 Helper Superclass 中添加一个 waitForCondition 方法来方便脚本开发。如代码清单 4 所示。
清单 4.
public interface Condition { boolean value(); } /** * 判断条件在指定时间内是否成立 * @param condition * @param timeout * @param interval * @return */ public static boolean isTrue(Condition condition, double timeout, double interval){ long startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime < timeout * 1000) { if (condition.value()) return true; sleep(interval); } return condition.value(); } /** * 等待条件成立。如果在指定时间内没有成立,将抛出异常 * @param condition * @param timeout * @param interval */ public static void waitForCondition(Condition condition, double timeout, double interval){ if (!isTrue(condition, timeout, interval)) throw new RuntimeException("Condition is not true!"); } |
RFT 在识别测试对象,模拟鼠标,键盘事件过程中,都使用了一些默认值。这些值能保证测试脚本在一台性能不错的测试机上成功执行,但遇到一台很慢的测试机,往往会失败。可以通过 API 来修改这些与时间相关的选项,适当的增加延迟让脚本更加健壮。这些选项的常量定义在 com.rational.test.ft.script.IOptionName 中。RationalTestScript.setOption()/resetOption() 可以改变这些选项,也可以通过 getOption() 来获得选项的值。例如被测程序不能处理过快的用户输入,可以如代码清单 5 所示改变输入文字的速度。表 1 中是 RFT 支持的有关时间的所有选项。
清单 5.
setOption(IOptionName.DELAY_BEFORE_KEY_DOWN, 1); // 等待 1 秒后,再按下按键 InputWindow().inputKeys("slow down"); resetOption(IOptionName.DELAY_BEFORE_KEY_DOWN); // 恢复默认值 |
表 1. 有关时间的选项
常量 | 说明 |
---|---|
DELAY_AFTER_WINDOW_ACTIVATE | 激活窗口后的延迟。如果初始化 GUI 需要很长时间,可以增加此值。 |
DELAY_BEFORE_GUI_ACTION | 执行 GUI 操作(例如 click, drag, inputKeys)前的延迟。 |
FLEX_DEFAULT_GUI_ACTION_DELAY | 对 FLEX 程序执行 GUI 操作前的延迟。 |
DELAY_BEFORE_KEY_DOWN | 按下按键前的延迟 |
DELAY_BEFORE_KEY_UP | 松开按键的延迟 |
DELAY_BEFORE_MOUSE_DOWN | 按下鼠标按钮前的延迟 |
DELAY_BEFORE_MOUSE_MOVE | 移动鼠标前的延迟 |
DELAY_BEFORE_MOUSE_UP | 松开鼠标前的延迟 |
MOUSE_HOVER_TIME | 鼠标 Hover 的时长 |
WAIT_FOR_EXISTENCE_DELAY_BETWEEN_RETRIES | 多长时间后继续尝试判断对象是否存在 |
MAXIMUM_WAIT_FOR_EXISTENCE | 尝试判断对象是否存在的最大次数 |
FIND_OBJECT_DELAY_BETWEEN_RETRIES | 多长时间后继续尝试查找对象 |
MAXIMUM_FIND_OBJECT_TIME | 尝试查找对象的最大次数 |
TIME_MULTIPLIER | 所有延迟的乘数。比如设置为 2,所有延迟都将增加 2 倍 |
您是否遇到过一上班发现夜里进行的自动化回归测试因为某个 RFT 脚本 freeze 了,使得剩余的其他脚本没有执行?导致这种情况发生的原因通常是 RFT 脚本调用远程方法的时候,AUT 死锁了使得远程方法无法返回。只要将 AUT 进程杀掉就可以让脚本继续运行。如果脚本能够设置一个超时时间就可以防止这类问题发生,然而 RFT 本身并不支持这个功能。不过通过几行代码就可以轻松实现这个功能。
我们将超时检测封装到 Helper Superclass 中,如代码清单 6 所示。测试脚本再初始化的时候会开启一个 Timer, 当脚本运行时间超出设定值后,onTimeout 方法就被调用,同时脚本的执行被终止。
清单 6.
/** * 支持 timeout 的 Helper Superclass */ public class TestScriptHelper extends RationalTestScript { private static Timer timer = new Timer(true); private static TimerTask timerTask = null; private long timeout = 0; /** * 设置脚本最长运行时间,单位为秒。* @param timeout 如果 timeout <= 0,脚本将不做超时检测 */ public void setTimeout(long timeout) { this.timeout = timeout; } public void onInitialize() { if (timeout > 0) { if (timerTask != null) timerTask.cancel(); timerTask = new TimerTask(){ public void run() { onTimeout(); // 终止脚本的运行 TestContext.getRunningTestContext().setAbort("Timeout"); } }; timer.schedule(timerTask, 1000 * timeout); } } public void onTerminate() { if (timerTask != null) { timerTask.cancel(); timerTask = null; } } /** * 子类可以通过覆盖该方法来做超时的清理工作 . */ public void onTimeout() { } } |
代码清单 7 中显示如何在测试脚本中使用超时检测。注意 DemoScript 的 Helper Superclass 为 TestScriptHelper,要在脚本的构造方法中通过 setTimeout 方法来指定超时时长。
清单 7.
package testcases; import resources.testcases.DemoScriptHelper; public class DemoScript extends DemoScriptHelper { public DemoScript() { setTimeout(3); } public void onTimeout() { System.out.println("DemoScript runs out of time!"); //TODO 清理工作,比如 kill 被测程序,删除用户数据等 } public void testMain(Object[] args) { sleep(20); //TODO 实现测试步骤 } } |
在 RFT 中,我们通常使用 callScript 来创建 test suite。在启用脚本超时检测后,直接调用 callScript 将带来一个问题。一旦某个脚本超时,抛出错误将导致整个 test suite 停止执行。我们可以用代码清单 8 所示来创建 test suite。其中关键点是要捕获超时时抛出的错误。
清单 8.
package testcases; import resources.testcases.DemoSuiteHelper; import com.rational.test.ft.script.RationalTestScriptError; public class DemoSuite extends DemoSuiteHelper { /** * 执行测试用例 * @param script * @param args */ public void runScript(String script, Object[] args) { try { callScript(script, args); } catch (RationalTestScriptError e) { // 捕获 Timeout 错误,防止整个测试被终止。 if (!"Timeout".equals(e.getMessage())) throw e; } catch (Exception e) { e.printStackTrace(); } } public void runScript(String script){ runScript(script, null); } public void testMain(Object[] args) { runScript("testcases.DemoScript"); runScript("testcases.DemoScript"); runScript("testcases.DemoScript"); } } |