很多朋友在刚接触 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、显式等待,等待条件灵活,代码量稍多,每次定位元素都需要单独调用。可使用封装的方式解决。
所以在我个人的实际工作中,会将显式等待和线程等待结合使用。而隐式等待则一般不用。


↙↙↙阅读原文可查看相关链接,并与作者交流