Selenium Selenium 隐式等待与显示等待的选择

fiskeryang · 2020年08月17日 · 最后由 fiskeryang 回复于 2020年09月11日 · 166 次阅读

很多朋友在刚接触 Selenium 隐式等待与显示等待时可能会有一些困惑,这两种方式到底有什么优劣,我们应该在何种情况下选择哪种等待方式?
下面我们来分析一下这它们各有什么特点。

一般来说,使用 selenium 实现自动化测试时可能会用到三种等待方式 :
1、Thread.sleep 线程等待
2、selenium 提供的隐式等待
3、selenium 提供的显式等待

首先,线程等待很简单,执行时会阻塞整个线程,而且必须要等到等待时间过完才能继续向下执行,一般我们在自动化测试中可以作为步骤执行之间的一个固定间隔来使用,比如每一步操作之间可以固定设一个 0.5~1 秒的间隔时间,以避免操作速度太快造成一些意料之外的问题。可以把它封装起来方便调用。

public static void sleep(int sec) {
    try {
        Thread.sleep((long)(sec * 1000));
    } catch (InterruptedException ) {
        .printStackTrace();
    }
}

其次,隐式等待。只要设置一次,在 WebDriver 实例的整个生命周期都是生效的,并且相对于线程等待,这个只要一旦发现了元素在 DOM 树中出现就可以继续向下执行。

driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);

写起来是挺方便的。但是我们来看一下 selenium 框架中对于 implicitlyWait 方法是如何描述的

/**
 * Specifies the amount of time the driver should wait when searching for an element if it is
 * not immediately present.
 * When searching for a single element, the driver should poll the page until the element has
 * been found, or this timeout expires before throwing a {@link NoSuchElementException}. When
 * searching for multiple elements, the driver should poll the page until at least one element
 * has been found or this timeout has expired.
 * Increasing the implicit wait timeout should be used judiciously as it will have an adverse
 * effect on test run time, especially when used with slower location strategies like XPath.
 */

寻找单个元素时,会寻找元素一直到找到或者超时。寻找多个元素时,找到至少一个符合条件的元素就会判定为成功继而向下运行。
隐式等待要谨慎使用,因为这会对测试运行时间产生不利影响,尤其是与 XPath 等较慢的定位策略一起使用时

那么问题来了,在真实的 UI 测试中我们经常会遇到一些这样的情况,并不是需要找到这个元素就执行,而是有各种不同的执行条件,比如
等待某个元素消失,比如进度条
等待元素的属性变化,比如 style,src,value 之类的属性变为期望的值
等待元素能从 DOM 树中找到并且可见、可操作
等待多个元素都符合期望的条件
多种复合条件需要同时满足
类似于这些条件,只是使用隐式等待已经无法满足我们的需求了。 那我们再来看看显式等待
首先需要实例化一个 WebDriverWait 对象 (有三个构造方法重载,我们选一种常用的构造方法)
三个参数分别是 driver 实例,超时时长 (秒),轮询间隔(毫秒)

WebDriverWait webDriverWait = new WebDriverWait(driver,5,1000);

然后调用 webDriverWait 的 until 方法,这个方法有两个重载对应的返回值和参数都不同

public void until(com.google.common.base.Predicate<T> isTrue)
public <V> V until(com.google.common.base.Function<? super T, V> isTrue)

下面我们分别看一下这两个方法的作用
第一个方法 返回值是 void 参数是一个 Predicate 接口,其作用是一直等待到 Predicate 中的 apply 方法返回 true 或者超时,再继续向下执行
我们来执行一下下面这段代码,看看会发生什么

webDriverWait.until(new Predicate<WebDriver>() {
    @Override
    public boolean apply(WebDriver webDriver) {
        System.out.println("Predicate等待");
        return false;
    }
});

console 输出:
Predicate 等待
Predicate 等待
Predicate 等待
Predicate 等待
Predicate 等待
Predicate 等待

一共打印了 6 次 Predicate 等待,说明一共轮询了 6 次中间间隔时间一共是 5 秒。
现在我们把上面代码的 apply 方法返回值改为 true,在运行一次看看

webDriverWait.until(new Predicate<WebDriver>() {
    @Override
    public boolean apply(WebDriver webDriver) {
        System.out.println("Predicate等待");
        return true;
    }
});

console 输出:
Predicate 等待

可以看出,只轮询了一次,因为 apply 返回了 true,跳出了轮询继续往下执行了。
所以使用 predicate 参数的这个 until 方法作用等待到符合用户指定的条件再向下执行。
并且,该方法没有返回值,也不会抛出异常,等待的最长时间就是用户设置的超时时长

第二个方法 返回值是一个泛型类型 V ,参数是一个函数接口 Function<? super T, V>
其中 ? super T 表示该参数必须是 T 或 T 的父类 V 表示该参数和返回值是相同的类型

WebElement ele = webDriverWait.until(new Function<WebDriver, WebElement>() {
    @Override
    public WebElement apply(WebDriver webDriver) {
        return driver.findElement(By.xpath(".//a[text()='新闻']"));
    }
});

这个显示等待方法的作用和隐式等待是类似的,会一直轮询直到找到符合定位条件的元素出现在 DOM 树中,并返回该元素的对象。
我们把它改造一下,换成一个找不到的元素看看

WebElement ele = webDriverWait.until(new Function<WebDriver, WebElement>() {
    @Override
    public WebElement apply(WebDriver webDriver) {
        System.out.println("尝试寻找元素");
        return driver.findElement(By.xpath(".//a[text()='新闻1']"));
    }
});

执行结果是 打印了六次 “尝试寻找元素” 后,抛出了一个 TimeOutException。
这样我们可以看出,显示等待的优势就是由用户自定义各种具体的等待条件,满足实际工作中的各种需求。
Selenium 也提供了一些预置的等待条件,是由 Function 的子接口 ExpectedCondition 的封装类 ExpectedConditions 来实现的,
我们来看看 ExpectedCondition 接口的定义

public interface ExpectedCondition<T> extends Function<WebDriver, T> {
}

可以看到 ExpectedCondition 与 function 的区别只是在于指定了第一个参数为 WebDriver 类型而已
ExpectedConditions 给开发者提供了许多内置的等待条件
常用的一般有以下这些

//等待元素可点击
webDriverWait.until(ExpectedConditions.elementToBeClickable(by));
//等待元素消失(不可见或从DOM树中消失都算)
webDriverWait.until(ExpectedConditions.invisibilityOfElementLocated(by));
//等待元素可见,光是找到元素不行,必须得能看到,元素的长宽不为0
webDriverWait.until(ExpectedConditions.visibilityOf(by));
//等待元素的属性包含指定的值
webDriverWait.until(ExpectedConditions.attributeContains("str"));
//等待所有元素可见
webDriverWait.until(ExpectedConditions.visibilityOfAllElementsLocatedBy(by));
//还有很多...

如果有个性化需求,比如需要同时满足多个不同的需求的话,就只能自己实现 ExpectedCondition 接口的 apply 方法来实现了
由此可见,显式等待相对隐式来说,功能要灵活得多。完全可以取代隐式等待的功能。只是相对来说,代码多了一点。我们可以采取封装的方式来解决
比如下面这个方法封装了等待元素可点击的操作,成功则返回元素对象,失败抛出异常

public static WebElement waitForElementClickable(WebDriver driver, final By by) throws Exception {
   WebElement element;
   try {
      element = new WebDriverWait(driver, 5, 1000).until(ExpectedConditions.elementToBeClickable(by));
   } catch (Exception e) {
      System.out.println("寻找元素失败");
      throw e;
   }
   return element;
}

使用时只需要调用 waitForElementClickable 这个方法就行了。
最后我们可以总结一下各种等待方式适用的场景
1、线程等待,简单粗暴,只适用于操作步骤之间的固定间隔,可提高页面操作的稳定性。不过数值设置大了会严重影响脚本的执行效率
2、隐式等待,使用简单,且设置一次后在指定 Driver 实例的生命周期中全局可用。但等待条件单一,且不适用于 xPath
3、显式等待,等待条件灵活,代码量稍多,每次定位元素都需要单独调用。可使用封装的方式解决。
所以在我个人的实际工作中,会将显式等待和线程等待结合使用。而隐式等待则一般不用。

共收到 4 条回复 时间 点赞

请教下楼主,隐式等待是要等到所有元素都加载完成才会去执行下一步的吗?

leesy1992 回复

在 DOM 树中能找到你寻找的元素就会继续,但这时元素还不一定能响应操作

楼主最后的总结非常到位,列出了 3 种类型等待的适用范围条件和利弊。同时也反映出一个问题:我们自动化测试工程师在实现和维护自动化测试的时候,应该把主要精力放在保证测试用例的业务逻辑 (包含结果验证) 正确性上,是不是不应该被所选工具提供的 workaround 分散或占用过多精力,是不是应该寻找成本更低效果更好的办法也就是真正意义上的 solution 呢?

在前年为某客户方提供测试技术咨询服务的时候,遇到过楼主的类似问题并给出过解决办法,这里分享出来。
等待,反映的是被测系统的性能表现,性能表现,既跟运行环境(这里指测试环境)有关,又跟被测系统本身有关。
我们首先应该解决测试环境问题,通过优化或升级解决等待问题更划算。比如被测系统运行期间,确保其他无关程序关闭,不装更好,只装被测系统及其依赖程序软件,即 “充分必要软测试环境” 或 “最小软测试环境”; 比如更换运算更快的 CPU,存取更快容量更大的内存,固态硬盘等,即 “最强硬测试环境”。硬件不值钱,人力才是最贵的。
测试环境不存在性能问题,则问题出在被测系统本身。如果系统反应时间慢不符合设计要求,可以提交性能缺陷,提高缺陷优先级,通过修复缺陷解决等待问题更划算。如果反应慢但符合设计需求则不是缺陷,或不符合设计需求是缺陷但放到以后再解决,这时候要不要使用等待有待商榷。某个、某些元素状态慢,是因为对应的接口执行慢(可能仍符合设计需求,前文已述),考虑使用 mock 来模拟对应接口的执行,mock 不需要等待。但使用 mock 有技术和其他成本,对比使用等待,可根据公司情况或项目具体情况做出取舍。

最后总结:尽量不用等待,消灭等待,如果可能。

Thirty-Thirty 回复

嗯 你说的也是我们期望达到的一种状态 只是应用到具体项目上还是得看实际的需求和场景来决定。
比如我司的产品是企业应用,自动化脚本不仅需要在公司内的测试环境使用 还需要部署到客户现场执行
而客户提供的环境是我们无法预料的,大多时候性能无法保障,甚至还需要和其他系统共享。所以我们的主要关注点在于测试框架与脚本的稳定性和减少脏数据引起的问题。同时也外挂了性能监控的插件,对于一些操作等待时间很长的步骤也会在测试报告中进行警告提示。但是 也只是如此了。 同样的脚本,在公司中和不同的客户环境中运行,执行时长可能大相径庭,只能做到尽量兼容。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册