
2.4 Selenium WebDriver
如果Selenium IDE已经让你度过了自动化测试的破冰期,那么接下来要学习的Selenium WebDriver会让你开阔视野,帮助我们处理更加复杂的测试场景。Selenium WebDriver提供了一套友好的API,支持Java、Python、Ruby和C#等多种编程语言来创建测试脚本。为了突出Selenium WebDriver的设计思想,弱化编程语言本身对测试脚本的影响,本节将从工作原理入手,在场景演练环节分别介绍Python和Java两门语言的入门示例。
2.4.1 工作原理
Selenium WebDriver是调用浏览器的原生接口来操作浏览器的。也就是说,测试脚本操作浏览器的过程就是在测试脚本中创建WebDriver对象,再通过这个对象调用WebDriver API来访问浏览器接口,从而操作浏览器的过程。
如图2-38所示,我们在测试脚本中使用Selenium WebDriver,无论是哪种平台、哪种浏览器,处理逻辑都是通过一个ComandExecutor发送命令,实际上就是一条发送给Web Service的HTTP请求。Web Service是基于特定WebDriver Wire协议的RESTful接口,测试脚本通知浏览器要做的操作都包含于发送给Web Service的HTTP请求体中。

图2-38 Selenium WebDriver实现原理
不同浏览器的WebDriver子类(FirefoxDriver、InternetExplorerDriver、ChromeDriver、SafariDriver等)都需要依赖特定的浏览器原生组件,例如Firefox需要附加组件webdriver.xpi。而IE需要用到一个dll文件来转化Web Service的命令为浏览器调用。
以下是Selenium HttpCommandExecutor类的部分代码。里面维护了一个Map,它会将简单的命令转化为相应的请求URL。当RESTful Web Service接收到HTTP请求后,它便解析出需要执行的操作。同时,代码还表现出:请求是基于sessionId的,这意味着不同WebDriver对象在多线程并行的时候不会有冲突和干扰。
nameToUrl = ImmutableMap.<String, CommandInfo>builder() .put(NEW_SESSION, post("/session")) .put(QUIT, delete("/session/:sessionId")) .put(GET_CURRENT_WINDOW_HANDLE, get("/session/:sessionId/window_handle")) .put(GET_WINDOW_HANDLES, get("/session/:sessionId/window_handles")) .put(GET, post("/session/:sessionId/url")) // The Alert API is still experimental and should not be used. .put(GET_ALERT, get("/session/:sessionId/alert")) .put(DISMISS_ALERT, post("/session/:sessionId/dismiss_alert")) .put(ACCEPT_ALERT, post("/session/:sessionId/accept_alert")) .put(GET_ALERT_TEXT, get("/session/:sessionId/alert_text")) .put(SET_ALERT_VALUE, post("/session/:sessionId/alert_text"))
Selenium WebDriver把这些逻辑都封装了起来,我们只需要关心driver对象的创建以及调用driver对象的哪个方法来操作页面元素。
2.4.2 元素定位
元素定位,即通过元素特有的属性唯一确定元素的过程。比如说,页面上有标签、文本框、按钮等多个元素,我们要编写脚本来实现按钮的点击操作。在编写Selenium WebDriver脚本之前,首先要了解如何让浏览器“知道”我们将要操作的是哪个元素。
1.常见的元素定位方法
页面中的元素定位是使用DOM元素属性进行实现的,使用Firefox浏览器的Firebug插件或Chrome浏览器的Inspector(检查器)可以很方便地来定位元素,并查看元素属性,如图2-39所示。

图2-39 使用Chrome Inspector查看元素属性
下面介绍常见的元素定位方法。注意,Selenium WebDriver脚本是基于WebDriver对象的,这里为了演示元素定位而跳过了WebDriver对象的创建,直接引用了WebDriver对象(driver)。2.4.3场景演练中有WebDriver对象的创建语句,第3章详细介绍了不同类型的WebDriver对象。
● ID:根据DOM元素的ID属性定位。例如以下元素及相应定位方法:
<div id="pingxx">...</div> WebElement element = driver.findElement(By.id("pingxx "));
● Name:根据DOM元素的Name属性定位。
<input name="pingxx" type="text"/> WebElement element = driver.findElement(By.name("pingxx"));
● ClassName:根据DOM元素的Class属性定位。例如以下元素及相应定位方法:
<div class="test"><span>Pingxx</span></div><div class="test"><span>Gouda</span></div> List<WebElement> tests = driver.findElements(By.className("test"));
● TagName:根据DOM元素的TagName定位。例如以下元素及相应定位方法:
<iframe src="..."></iframe> WebElement frame = driver.findElement(By.tagName("iframe"));
● LinkText:根据DOM元素(link)的文本内容定位。例如以下元素及相应定位方法:
<a href="http://www.google.com/search? q=pingxx">searchPingxx</a> WebElement cheese = driver.findElement(By.linkText("searchPingxx"));
● PartialLinkText:根据DOM元素(link)的部分文本内容定位。例如以下元素及相应定位方法:
<a href="http://www.google.com/search? q=pingxx">search for Pingxx</a> WebElement cheese = driver.findElement(By.partialLinkText("Pingxx"));
● CssSelector:根据CSS选择器定位元素。
<div id="food"> <span class="dairy">milk</span> <span class="dairy aged">cheese</span> </div> WebElement cheese = driver.findElement(By.cssSelector("#food span.dairy.aged"));
● XPath:使用XPath定位元素。在Firefox浏览器中可以下载firebug插件或firepath插件来直接获得元素的XPath。
<input type="text" name="example" /> <input type="text" name="other" /> List<WebElement> inputs = driver.findElements(By.xpath("//input"));
2.如何选择定位方法
前面介绍了多种元素定位的方法,可谓“条条大道通罗马”。那么,在实际项目中,如何选择最适合的定位方法呢?这是测试脚本的编写者经常会面临的问题。
(1)当页面元素存在id属性时,一般使用id来定位,因为id具有唯一性,可以直接定位到这个元素。然而,在实际项目中,有时会出现缺少标准属性(例如没有id属性或者某些元素的id是动态的)、页面刷新会改变等情况,这时就只能选择其他属性来定位。
(2)如果这个元素不存在诸如id这类唯一值的属性,也就是说,我们不方便通过某个值直接定位到这个元素。那么,可以转变思路:先找到一类元素,再通过具体的顺序位置定位到某一个元素。一般在这种情况下,可以考虑用TagName或ClassName。当有链接需要定位时,可以考虑linkText或partialLinkText方式。代码里可以直接看到链接的文本内容,增强代码的可读性,便于维护。
(3)XPath定位很强大,但定位性能不是很好,且可读性也会变差。例如这一段代码:
driver.findElement(By.id("btn_login")).click();
上述语句可以很明确地看出是在单击“登录”按钮进行登录操作,若使用XPath,这句则变成:
driver.findElement(By.xpath(“/html/body/div[2]/div/div/div[3]/a[7]”)).click();
显然后者的表述形式不够简明。另一方面,如果页面上的元素位置做了调整,即元素路径变了,那么使用XPath的脚本也要调整,这说明大量使用XPath的测试脚本维护成本很高。因此建议只有在少数元素不好定位的情况下,选择XPath或cssSelector。
(4)上面提到的均是Selenium WebDriver自带的元素定位方法,在一些特殊情况下,它们或许都无法满足我们的需求。但是,我们可以另辟蹊径,用JavaScript来操作元素。Selenium WebDriver提供了执行JavaScript语句的方法,可以直接调用。由于这种方式需要一定的JavaScript功底,笔者这里不做详细介绍,7.2.2小节代码示例中有所涉及。
2.4.3 场景演练
下文将以“Bing搜索”为例介绍Selenium WebDriver编写测试脚本的整个过程。采用Java和Python两门语言作为示例代码。
1.环境准备
(1)确保编程语言的运行环境是可用的。
● Java:不论你是否要写Java代码,最好都先准备JRE环境,2.2小节提到过RemoteWebDriver,它依赖一个启动Jar包Selenium Server,而在2.4节中我们也会用到Jar包。访问http://www.oracle.com/technetwork/java/javase/downloads可下载最新版本的JDK。
● Python:如果你选择Python作为测试脚本的语言,可访问https://www.python.org/downloads/下载安装包。由于Python 2与Python 3不能完全兼容,请选择合适的Python版本下载。本书的示例代码均在2.7.8版本中执行成功。
(2)下载该编程语言对应的Selenium包,又称为Selenium Language Binding Package,如图2-40所示。下载地址:http://docs.seleniumhq.org/download/。如果你使用Python,并且安装了pip,可使用pip install selenium命令下载。

图2-40 WebDriver各个语言包的下载页面
(3)目标浏览器以及相应的Driver文件。因浏览器的不同,Driver文件的形式和用法也不同。各个Driver对象的介绍将在第3章中详细说明。下文示例中将使用FireFoxDrvier,无须额外下载Driver文件,只需要Firefox浏览器安装完成即可。
2. Python篇:Selenium WebDriver脚本
测试场景:在http://cn.bing.com/中搜索关键字WebDriver。
具体步骤:
步骤 01 在脚本中导入Selenium Python包。
步骤 02 创建Firefox的WebDriver对象。
步骤 03 调用get方法打开Bing页面。
步骤 04 找到搜索文本框,输入WebDriver。
步骤 05 单击“搜索”按钮。
步骤 06 调用quit方法关闭页面,结束测试。
from selenium import webdriver driver =webdriver.Firefox() driver.get("http://cn.bing.com/") driver.find_element_by_id("sb_form_q").send_keys("WebDriver") driver.find_element_by_id("sb_form_go").click() driver.quit()
将上述代码保存为Python文件即可执行。这是一个最简单的示例,既没有考虑页面加载所需的等待时间,也没有考虑输出测试结果。希望通过这个示例能让你感受到Selenium WebDriver的入门非常简单。至于下面的Java篇,是希望让你了解,对于不同的编程语言,Selenium WebDriver的使用也非常类似。无论你在实际工作中使用的是Python还是Java,抑或是其他语言,本书各个示例都有一定的参考价值。
3. Java篇:Selenium WebDriver脚本
(1)创建测试用例
使用Selenium WebDriver原本与单元测试框架并没有直接的关系,本节引入JUnit单元测试框架,可以方便生成报告、进行代码注解等。
在Eclipse的测试项目中新建一个JUnit Test Case,选择New JUnit 4 test类型创建,如图2-41所示。Eclipse一般会自带JUnit4的Jar包,只需在单击Finish后弹出的窗口中单击“确定”按钮即可。如图2-42所示,JUnit包已经添加到项目中了。

图2-41 新建Test Case

图2-42 测试项目的文件结构
(2)编写测试脚本
创建成功后就可以在TestBing.java中写入测试代码了,代码如下。这里为了方便,使用了Thread.sleep(2000);作为等待处理,关于Selenium WebDriver的Wait写法,参见2.4.4小节。
package com.selenium.test; import org.junit.*; import org.openqa.selenium.*; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.firefox.FirefoxDriver; public class TestBing{ private WebDriver driver; private String baseUrl; @Before public void setUp()throws Exception{ driver=new FirefoxDriver(); baseUrl="http://cn.bing.com/"; } @Test public void testBing()throws Exception{ driver.get(baseUrl); Thread.sleep(2000); driver.findElement(By.id("sb_form_q")).sendKeys("WebDriver"); driver.findElement(By.id("sb_form_go")).click(); } @After public void tearDown()throws Exception{ driver.quit(); } }
(3)运行测试脚本
运行此程序时,需要在TestBing.java上右击,然后在弹出的快捷菜单中依次单击Run As→JUnit Test。
以上述代码为例,若执行成功,则会弹出火狐浏览器,自动打开必应首页并输入关键字进行搜索,之后浏览器自动退出,测试结束,如图2-43所示。

图2-43 脚本的执行结果
2.4.4 Wait
在2.3.3小节Selenese中,我们已经了解了Selenium IDE提供的Wait方法。接下来,将学习Wait在Selenium WebDriver中的写法。
1.显式等待
显式等待(Explicit Wait)就是提供了明确的等待条件,若条件满足,则不再等待,继续执行后续代码。例如,我们需要清空id属性为sb_form_q的文本框内容,用Java代码可以写为:
driver.findElement(By.id(“sb_form_q”)).clear();
为了避免在页面加载过程中因sb_form_q元素未出现导致代码执行失败,我们可以增加Wait条件,等待sb_form_q元素出现之后,再做清空操作。其代码如下:
WebElement searchElement = (new WebDriverWait(driver,10)).until(new ExpectedCondition <WebElement>(){ public WebElement apply (WebDriver wd){ return wd.findElement(By.id("sb_form_q")); } }); searchElement.clear();
在上述代码中,时间设置为最多等待10秒,超时会抛出异常TimeoutException。在这10秒内,WebDriverWait默认每隔500毫秒调用一次ExpectedCondition来确认是否找到元素。
2.隐式等待
隐式等待(Implicit Wait)没有明确设定在何时开始等待,它相当于设置了全局范围的等待,对所有元素设置等待时间,在WebDriver对象的整个生命周期中生效。若不设置,则默认为0。
代码如下,在初始化方法中,设置了全局30秒等待时间:
@Before public void setUp() throws Exception { driver = new FirefoxDriver(); baseUrl = "https://www.xxx.com"; driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS); }
2.4.5 常用的断言
表2-1罗列了Selenium WebDriver常用的断言方法,并附上Selenium IDE中对应的Selenese命令作为对照。
表2-1 Selenium WebDriver常用的断言方法

(续表)
