Selenium与浏览器自动化
自动化测试的意义在于降低人工、时间成本,在需要重复执行测试用例的场景下——例如回归测试、压力测试、随机性缺陷重现——自动化测试的优势很明显。
自动化测试也存在一些重要的缺点:
- 对技术能力有要求,测试人员必须有能力编写测试脚本
- 测试脚本本身可能引入缺陷
- 测试脚本必须和需求/设计同步。在快速变化的项目中,维护测试脚本的成本很高,可重用性差
- 测试人员的经验、能力是不可替代的,这些不能完全的映射到测试脚本中
UI的自动化测试具有以下额外的缺点:
- 无法代替人类的审美能力
- 复杂的UI逻辑导致测试脚本难以编写
- UI中会包含一些刻意禁止自动化的逻辑,例如验证码
自动化测试,特别是UI自动化测试,不能代替人工测试。尽管如此,使用优秀的自动化测试工具辅助UI测试,仍然能够提高工作效率。多大比例的用例进行自动化测试,取决于团队对上述优缺点的权衡。
Selenium是一套浏览器自动化工具,也就是说,它能够通过脚本来模拟用户和浏览器的交互,并判断交互的结果。很多浏览器以扩展的方式支持Selenium,Selenium也是很多其它浏览器自动化工具、框架的核心组件。
另外一类Headless(没有图形界面)浏览器自动化工具,例如Phantomjs,某些场景下可以代替Selenium。相比之下Selenium的优势是支持多种真实的浏览器,而Phantomjs是基于Webkit引擎的,仅部分兼容某些浏览器。
如果需要更通用的UI自动化工具,可以考虑Sikuli。
UI自动化测试是Selenuim的主要用途,但是你也可以使用它来进行一些重复的Web管理工作。
Selenium提供两个可单独使用的组件,以满足不同的需求:
组件 | 说明 |
Selenium WebDriver |
也叫Selenium 2,支持多种语言的浏览器“驱动”,通过这些驱动你可以编写程序,模拟人工操作,操控浏览器。如果你需要:
你可以选择该组件 WebDriver是 Selenium Remote Control(Selenium 1)的继任者,后者已经被废弃 WebDriver支持Chrome、IE7-11、Firefox等主流浏览器。安装appium后Android、iOS浏览器也被支持 |
Selenium IDE |
Firefox的一个扩展,能够录制-回放你与浏览器的交互过程。如果你需要:
你可以选择该组件 |
Selenium 2最重要的特性是集成了WebDriver API。该API具有多种编程语言的绑定(Binding),它一方面简化了接口,另一方面解决了Selenium RC的一些限制。WebDriver对动态页面比较友好,这类页面的元素在可以不发生reload的情况下发生改变。
你可以使用编程语言/平台下常用的包管理工具/构建工具来获得WebDriver API,以Java和Python为例:
依赖库存放在Maven中心仓库,把下面的依赖加入到你的Maven工程中即可:
1 2 3 4 5 |
<dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>3.0.0-beta2</version> </dependency> |
Python的版本必须是2.7或者3.2+。执行下面的命令,安装WebDriver的Python绑定:
1 |
sudo pip install selenium |
WebDriver API通过驱动,调用各浏览器原生的自动化支持组件,这些组件的接口以浏览器而不同,Selenium驱动负责屏蔽这种不同。你需要为所有需要支持的浏览器安装相应的驱动。
Selenium-RC则通过JavaScript注入的方式工作,当页面加载完成后, Selenium-RC注入一系列测试代码并运行。
你需要为需要自动化的目标浏览器安装WebDriver的“驱动”。
为了便于理解这个“驱动”的功能,我们可以用JDBC来类比。JDBC API和WebDriver API的角色类似,而JDBC驱动和WebDriver驱动的角色类似,后者都是前者的实现。不同的是,JDBC驱动在客户端(相对于数据库,客户端是JVM)运行,而WebDriver驱动则完全在服务端(相对于浏览器,客户端是执行测试脚本的进程)。
驱动直接调用浏览器原生组件,因此它的实现必然依赖于浏览器,我们需要为需要支持的浏览器分别安装驱动。
HtmlUnit是一个基于Java实现的浏览器,它没有GUI。你可以通过HtmlUnit的API来与之交互,实现注入点击、填写表单之类的操作(这里可以看到,它做的事情和WebDriver API差不多,只是WebDriver API针对所有主流浏览器进行抽象)。
HtmlUnit使用的JavaScript引擎是Rhino,与主流浏览器不一样,因而它的行为可能和浏览器差异较大。另一方面,HtmlUnit驱动是WebDriver最快的一种实现。
HtmlUnit主要用于测试,但是它不是独立的测试框架,通常你需要联用JUnit、TestNG等测试框架。
由于HtmlUnit本身是基于Java的,因而在使用Java绑定时,自然不需要“安装”什么东西,只需要进行JVM内部的函数调用:
1 |
WebDriver driver = new HtmlUnitDriver(true); // 入参 true 表示启用JavaScript支持 |
如果使用其它语言的绑定,则需要借助于Selenium服务器:
1 |
driver = webdriver.Remote("http://localhost:4444/wd/hub", webdriver.DesiredCapabilities.HTMLUNITWITHJS) |
Chrome驱动是一个独立运行的程序,它实现了WebDriver的Wire协议。你只需要下载和操作系统匹配的压缩包,把chromedriver文件放置在环境变量PATH的某个目录中即可完成安装。如果不把chromedriver放在PATH中,你必须在:
- 对于Java,通过系统属性 webdriver.chrome.driver 指定chromedriver的绝对路径
- 对于Python,在构造WebDriver实例时提供参数:
1driver = webdriver.Chrome('/path/to/chromedriver')
WebDriver API会调用chromedriver,而后者会寻找系统上安装的Chrome。chromedriver默认认为Chrome安装在:
平台 | 安装位置 |
Linux | /usr/bin/google-chrome |
Mac | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome |
Windows | C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe |
如果你的Chrome不在上述标准位置,你需要在调用WebDriver API时指定:
1 2 |
ChromeOptions options = new ChromeOptions(); options.setBinary("/path/to/other/chrome/binary"); |
ChromeOptions类用于配置一个ChromeDriver会话具有的特性 。并不是所有绑定都具有ChromeOptions类(例如Ruby),你可以使用DesiredCapabilities代替之。下面继续列出ChromeOptions的一些其它用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 添加有扩展 options.addExtensions(new File("/path/to/extension.crx")); // 设置使用的Profile,默认情况下,ChromeDriver为每个Session创建一个临时的Profile options.addArguments("user-data-dir=/path/to/your/custom/profile"); // 以最大化方式启动Chrome options.addArguments("start-maximized"); // 为当前Profile指定偏好设置 Map<String, Object> prefs = new HashMap<String, Object>(); // 打开Profile目录下的Preferences文件,可以查看能够设置那些偏好 prefs.put("profile.default_content_settings.popups", 0); options.setExperimentalOption("prefs", prefs); |
你也可以通过DesiredCapabilities类来实现上述逻辑:
1 2 3 |
DesiredCapabilities capabilities = DesiredCapabilities.chrome(); ChromeOptions options = new ChromeOptions(); capabilities.setCapability(ChromeOptions.CAPABILITY, options); |
最后,你需要传递options/capabilities给ChromeDriver类的构造方法,创建WebDriver实例:
1 2 |
WebDriver driver = new ChromeDriver( options ); WebDriver driver = new ChromeDriver(capabilities); |
上面的示例代码中,ChromeDriver都是由Java程序调用的,实际上,ChromeDriver还可以作为远程服务器运行。由于ChromeDriver实现了Wire协议,因此它和任何 RemoteWebDriver 兼容。
执行命令启动ChromeDriver服务器:
1 2 3 4 |
# 默认监听端口 9515 # 默认仅允许本机访问,要允许其它IP访问,需要指定白名单 # --verbose 打印更多的日志 chromedriver --port=9515 --whitelisted-ips=192.168.0.89,172.16.87.132 --verbose |
构建WebDriver时,使用下面的代码:
1 |
WebDriver driver = new RemoteWebDriver("http://localhost:9515", DesiredCapabilities.chrome()); |
IE驱动和Chrome驱动类似,是一个独立的可执行文件,需要放置到PATH下,同样支持独立运行。这里不去描述更多细节,请参考官方文档。
依据使用WebDriver的方式,你可能需要或者不需要Selenium服务器。如果你的浏览器和测试代码在同一台机器上运行,并且仅仅使用WebDriver API,那么你不需要服务器。
如果你:
- 希望通过Selenium-Grid把测试脚本分发到多台机器上
- 希望连接到特定的远程机器,该机器具有特殊的浏览器版本
- 希望使用HtmlUnit Driver而又不使用Java绑定
那么,你需要安装Selenium服务器。
Chrome、IE驱动本身就可以作为服务器独立运行,因此你在使用这两款浏览器时,直接下载它们的驱动即可。
如果使用其它浏览器,可以下载Selenium Standalone Server,这是一个JAR,执行下面的命令运行服务器:
1 2 3 |
java -jar <path_to>/selenium-server-standalone-<version>.jar -Dwebdriver.enable.native.events=1 # 使用Native事件功能 -help # 查看帮助 |
编写脚本时,需要使用 RemoteWebDriver 。
完整的API文档链接:
本章内容以Python绑定为例,介绍WebDriver的API。
下面是从登录某系统、跳转到用户管理模块,然后添加用户等一系列操作对应的WebDriver代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
from time import sleep import time from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.wait import WebDriverWait if __name__ == '__main__': # 初始化WebDriver srv_args = [ '--verbose', '--log-path=/home/alex/Python/projects/pycharm/selenium-study/chromedriver.log' ] options = Options() options.add_argument("user-data-dir=/home/alex/.config/webdriver/chrome") driver = webdriver.Chrome(service_args=srv_args, chrome_options=options) # 在当前会话中加载页面 driver.get("http://192.168.0.221:8080/pems-web-manager/logon") try: print(driver.title) # 打印页面标题 user_name_input_id = 'userName-inputEl' # 需要等待登陆页面的动画效果执行完毕,才能看到登陆按钮 # 等待条件:用户名文本框可见(已经显示且宽高大于0)达成,最多等待5秒 wait = WebDriverWait(driver, 5) wait.until(EC.visibility_of_element_located((By.ID, user_name_input_id))) # 模拟输入用户名密码 user_name_input = driver.find_element_by_id(user_name_input_id) user_name_input.send_keys('system') passwd_input = driver.find_element_by_id('password-inputEl') passwd_input.send_keys('kingsmart') # 模拟点击登陆按钮 logoin_btn = driver.find_element_by_id('loginBtn-btnEl') logoin_btn.click() # 直到用户名标签变为非“未登录”,说明登录成功,可以进行后续操作 wait.until(lambda d: d.find_element_by_css_selector('div[id="userNameText"]').text != '未登录') driver.find_element_by_id('configSettingBtn').click() # 页面将发生导航,等待导航完成。注意,文本节点不能通过CSS选择器查找,只能通过XPath # //span[text()="系统管理"] 表示完全匹配,下面使用的XPath则表示包含 sys_mgr_link = wait.until(EC.visibility_of_element_located((By.XPATH, '//span[contains(text(),"系统管理")]'))) user_mgr_xpath = '//*[contains(text(),"用户管理")]' # 使用通配符,不易被实现干扰,尽量从人的视觉角度出发 user_mgr_link = driver.find_element_by_xpath(user_mgr_xpath) if user_mgr_link.is_displayed(): # 如果用户管理链接可见 ActionChains(driver).click(user_mgr_link).perform() # 另一种模拟点击的方法 else: sys_mgr_link.click() wait.until(EC.visibility_of_element_located((By.ID, user_mgr_link.id))).click() finally: # 退出,浏览器关闭 driver.quit() |
可以看到,WebDriver API是比较简单易懂的,适合研发经验不足的测试人员。
1 2 3 4 5 6 |
# 加载页面 driver.get("https://gmem.cc") # 前进 driver.forward() # 后退 driver.back() |
1 2 3 4 5 6 7 8 9 10 11 |
# 切换到已命名的窗口 driver.switch_to.window("windowName") # 可以基于窗口的handle进行切换,例如,下面依次切换到所有打开的窗口 for handle in driver.window_handles: driver.switch_to.window(handle) # 切换框架页(包括iframe) driver.switch_to.frame("frameName") # 切换到弹窗 alert = driver.switch_to.alert alert.dismiss() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# 添加Cookie driver.add_cookie({ 'name': 'key', 'value': 'value', 'path': '/', # 可选键值 'domain': 'blog.gmem.cc', 'secure': False, 'expiry': 1471329445 }) # 遍历Cookies for cookie in driver.get_cookies(): print "%s -> %s" % (cookie['name'], cookie['value']) # 删除Cookie driver.delete_cookie("CookieName") # 清空Cookie driver.delete_all_cookies() |
1 2 3 4 5 6 7 8 9 10 11 |
# 点击操作 el = driver.find_element_by_name("el") el.click() # 另一种方式 ActionChains(driver).click(el).perform() # 拖拽操作 from selenium.webdriver.common.action_chains import ActionChains element = driver.find_element_by_name("source") target = driver.find_element_by_name("target") ActionChains(driver).drag_and_drop(element, target).perform() |
selenium.webdriver.common.action_chains.ActionChains 类提供了大量模拟用户操作的方法,包括双击、点击并按住、按下按键等等。本文不逐一列举,请参考API文档。
WebDriver类提供了很多方法用于定位UI元素。注意这些方法可以针对WebElement进行调用,可以用来寻找该元素的子代元素:
方法 | 说明 | ||
find_elements_by_class_name() | 根据CSS类名获得UI元素:
|
||
find_element_by_tag_name() | 根据表签名获得UI元素:
|
||
find_element_by_name() | 根据name获得表单元素:
|
||
find_element_by_link_text() find_element_by_partial_link_text() |
根据href属性或者该属性的一部分获得链接元素:
|
||
find_element_by_css_selector() | 通过CSS选择器获得匹配的元素:
|
||
find_elements_by_xpath() | 通过XPath表达式获得匹配的元素:
|
1 2 3 4 5 6 |
el = driver.find_element_by_id('el') el.id # id属性 el.tag_name # 元素标签 el.rect # 返回表示元素大小和位置的字典 el.text # 获取文本节点 el.parent # 得到父WebElement的引用 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 下拉列表操作 select = driver.find_element_by_tag_name("select") allOptions = select.find_elements_by_tag_name("option") for option in allOptions: if option.text == 'China': option.click() break # 下拉列表操作,取消选择并重新选择 from selenium.webdriver.support.ui import Select select = Select(driver.find_element_by_tag_name("select")) select.deselect_all() select.select_by_visible_text("Edam") # 文本框操作 text = driver.find_element_by_id('text') text.send_keys('hello') # 可以针对表单内任何元素调用,以提交表单: element.submit() |
你可以让WebDriver执行任意脚本:
1 |
element = driver.execute_script("return $('.cheese')[0]") |
Leave a Reply