Selenium
Selenium 是支持 Web 浏览器自动化的一系列工具和库的 开源免费 综合项目。
支持模拟用户与浏览器的交互,它实现了 W3C WebDriver 规范的基础结构。
- 简洁的面向对象 API。
- 有效地驱动浏览器。
基本使用流程就是下载浏览器的对应版本的驱动程序并添加到 PATH 环境 中,然后使用自己擅长的开发语言编写自动化测试代码。
支持语言:
Java
、Python
、CSharp
、Ruby
、JavaScript
、Kotlin(Java)
常用类属性和方法
WebDriver
get
打开一个浏览器页面,参数是一个 URL 地址。
# 定义
def get(self, url: str) -> None
pass
driver.get("https://www.baidu.com")
find_element & find_elements
通过制定的元素定位方式查找页面元素,前者是指返回第一个匹配的元素,后者返回所有匹配元素的列表。
# 定义
def find_element(self, by=By.ID, value: Optional[str] = None) -> WebElement:
pass
def find_elements(self, by=By.ID, value: Optional[str] = None) -> List[WebElement]:
pass
switch_to
用于切换到指定 window、frame、alert。
window
切换到指定的窗口
- 可通过
driver.current_window_handle
获取当前窗口的标签(handle)。 - 可通过
driver.window_handles
获取所有窗口的(handle)
new_window
打开并切换到一个新的窗口或者Tab 页面,可通过参数配置,默认由浏览器自动选择。
# 打开一个 tab 页
driver.switch_to.new_window("tab")
# 打开一个新的窗口
driver.switch_to.new_window("window")
frame
切换到指定的 frame。
# 切换到指定 iframe 元素
driver.switch_to.frame(iframe_el)
# 通过 iframe 的 name 属性来切换,重名的第一个有效。
driver.switch_to.frame(frame_name)
# 通过索引切换 frame,第一个 iframe 的索引是 0
driver.switch_to.frame(0)
parent_frame
切回到父级 frame。
default_content
将上下文切换回到顶层文档。
save_screenshot
将当前浏览器窗口截图保存到指定文件位置。
还有三个和截图相关的方法:
get_screenshot_as_file
:save_screenshot
实际调用的方法。get_screenshot_as_base64
: 获取截图的 base64 编码字符串。get_screenshot_as_png
: 获取截图的字节数据列表。
execute_scripts
用来执行 JavaScript 脚本。
execute_scripts(script, *args)
等价于:
function execute() {
// script
// 可通过 arguments[index] 访问传入的参数
}
execute(...args)
WebElement
send_keys
模拟在元素中打字,对于 file input 还可以直接指定本地文件路径进行文件选择。
click
单击该元素。
元素定位
Selenium 支持 8 种基本的元素定位方式和 5 中相对定位方式。
基本元素定位
这八种元素定位方式分别是 ID
、NAME
、TAG_NAME
、CLASS_NAME
、LINK_TEXT
、PARTIAL_LINK_TEXT
、CSS_SELECTOR
、XPATH
。
ID
通过元素的唯一 ID 进行定位。
driver.find_element(By.ID, "kw")
NAME
通过元素的 name 属性进行定位。
TAG_NAME
通过元素的标签名进行定位。
CLASS_NAME
通过元素 class 属性进行定位。
driver.find_element(By.CLASS_NAME, "class1")
# 多个类名需要使用 . 拼接
driver.find_element(By.CLASS_NAME, "class1.class2")
LINK_TEXT
通过超链接的文本进行匹配。
PARTIAL_LINK_TEXT
通过超链接的部分文本进行匹配。
CSS_SELECTOR
通过 CSS 选择器进行匹配。
XPATH
通过 XPATH 进行匹配。
- 优点:能定位到任何元素。
- 缺点:执行效率低。
相对定位
这五种相对定位分别是 above
、below
、to_left_of
、to_right_of
、near
。
above
:查找在指定元素上方的元素。below
:查找在指定元素下方的元素。to_left_of
:查找在指定元素左边的元素。to_right_of
:查找在指定元素右边的元素。near
:在在指定元素周围的元素。
示例
这里使用本地 HTML 文件进行测试:
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>相对定位测试</title>
</head>
<body>
<div>
<input type="text" id="email" placeholder="email">
<br>
<input type="text" id="password" placeholder="password">
</div>
<div>
<button id="reset" onclick="alert('重置')">重置</button>
<button id="submit" onclick="alert('提交')">提交</button>
</div>
</body>
</html>
然后就可以通过相对定位找到需要的元素(这里都包含了 ID 方便测试)
# 查找在密码输入框上面的邮箱输入框
email_el = driver.find_element(locate_with(By.TAG_NAME, "input").above(driver.find_element(By.ID, "password")))
# 查找在邮箱输入框下面的密码输入框
pwd_el = driver.find_element(locate_with(By.TAG_NAME, "input").above(driver.find_element(By.ID, "email")))
# 查找在提交按钮左边的重置按钮
reset_el = driver.find_element(locate_with(By.TAG_NAME, "button").to_left_of(driver.find_element(By.ID, "submit")))
# 查找在重置按钮右边的提交按钮
submit_el = driver.find_element(locate_with(By.TAG_NAME, "button").to_left_of(driver.find_element(By.ID, "reset")))
特殊处理
下拉框
一般情况下可通过元素定位直接点击,也可以通过 Select
类进行处理。
- 支持通过索引(0 为起始值)、value 属性值、可见文本选择。
- 支持通过上述三种方式取消选择,同时也支持取消全部选择。
from selenium.webdriver.support.select import Select
select = Select(select_el)
select.select_by_index(1) # 通过索引选择
select.select_by_value("1") # 选择指定 value 值
select.select_by_visible_text("选项一") # 选择指定可见文本
弹窗处理
通过 switch_to 的 alert
属性进行处理。
driver.switch_to.alert.accept() # 确定
driver.switch_to.alert.dismiss() # 取消
driver.switch_to.alert.send_keys() # 输入 prompt 弹窗内容
鼠标和键盘事件(ActionChains)
ActionChains 用于配置一个操作执行链,通过 perform 执行。
点击
ActionChains(driver).click(element).perform()
双击
ActionChains(driver).double_click(element).perform()
右键
ActionChains(driver).context_click(element).perform()
按住键盘某个键并操作
from selenium.webdriver.common.keys import Keys
ActionChains.key_down(Keys.CONTROL).click(element).key_up(Keys.CONTROL).perform()
移动到元素(悬浮)
ActionChains(driver).move_to_element(element).perform()
键盘操作
from selenium.webdriver.common.keys import Keys
ActionChains(driver).send_keys(Keys.ENTER).perform()
等待方式
sleep
使用 time 模块下的 sleep 进行固定的等待时间。
from time import sleep
# 休眠两秒
sleep(2)
隐式等待
调用 WebDriver.implicitly_wait
方法设置默认的隐式等待时间。
# 隐式等待 2s,2s 内找到了元素会提前返回。
driver.implicitly_wait(2)
显式等待
使用 WebDriverWait
类实现显式等待。
WebDriverWait(
# WebDriver 实例
driver: Any,
# 等待超时时间,单位秒
timeout: float,
# 多久检测一次,默认值是 0.5 秒
poll_frequency: float = POLL_FREQUENCY,
# 调用期间忽略的异常类的可迭代结构。默认情况下仅忽略 NoSuchElementException
ignored_exceptions: Iterable[Type[Exception]] | None = None
)
# 可以使用 selenium 提供的检测方法
from selenium.webdriver.support import expected_conditions as EC
# 期待有两个窗口
EC.number_of_windows_to_be(2)
浏览器相关
主流浏览器驱动下载
浏览器 | 下载地址 | 扩展选项 |
---|---|---|
Chrome | Chrome >= 115 Chrome < 115 | ChromeOptions |
Edge | Web Driver | EdgeOptions |
Firefox | geckodriver | firefoxOptions |
浏览器配置
无头模式
没有图形用户界面 (GUI) 的 Web 浏览器。
⚠️ 限制
使用无头模式对浏览器窗口大小的操作时将无效,即无法最大化窗口。
Chromium
通过添加启动选项 --headless
开启。
版本要求:>= 59
Chrome 推出新的无头模式
官方说明:Chrome 的无头模式升级了:推出 --headless=new
Chrome 112 提供新的无头模式,在此模式下,Chrome 会创建但不显示任何平台窗口。所有其他功能(包括现有和未来的功能)均不受限制。
需要配置启动选项为 new 即 --headless=new
。
浏览器启动窗口大小
Chrome
通过添加启动选项 --window-size=1920,1080
进行配置。
使得 JS 无法检查
Chrome
# 关闭 Chrome 正受到自动化软件控制显式
options = webdriver.ChromeOptions()
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)
driver = webdriver.Chrome(options=options)
# 清除 navigator.webdriver 属性
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""})
封装 Selenium
用字符串代替 Selenium 的定位器
虽然 Selenium 的定位器默认就是字符串,但是部分长度较长,且没有简写方式,如果需要将测试步骤写为配置文件则不太方便,通用自定义套映射规则来解决这个问题。
Selenium 的定位其在 python 中的默认字符串为:
class By:
ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"
自定义映射规则:
自定义符号 | 定位器 |
---|---|
i 、id | id |
cls 、class | class name |
n 、name | name |
t 、tag | tag name |
l 、link | link text |
pl 、parital_link | partial link text |
c 、css | css selector |
x 、xpath | xpath |
def __selector2locator(selector: str) -> (By, str):
selector = [s.strip() for s in selector.split(",", 1)]
if len(selector) != 2: raise Exception("错误的选择器格式")
key, selector_value = selector
match key:
case "i" | "id":
return By.ID, selector_value
case "n" | "name":
return By.NAME, selector_value
case "cls" | "class":
return By.CLASS_NAME, selector_value
case "t" | "tag":
return By.TAG_NAME, selector_value
case "l" | "link":
return By.LINK_TEXT, selector_value
case "pl"| "partial_link":
return By.PARTIAL_LINK_TEXT, selector_value
case "c" | "css":
return By.CSS_SELECTOR, selector_value
case "x" | "xpath":
return By.XPATH, selector_value
case _:
raise Exception("不支持的选择器格式")
POM 设计模式
POM 设计模式即 Page Object Model。
每一个页面定义的一个类,类中包含页面的元素和相关操作方法。
POM 设计模式旨在为每个待测试的页面创建一个页面对象,将那些繁琐的定位操作封装到这个页面对象中,只对外提供必要的操作接口。
POM 分层
- 表现层:页面的可见元素。
- 操作层:对页面上的元素进行操作。
- 业务层:执行测试的详细步骤。
核心
- 定义
BasePage
用来封装 Selenium 的基本操作。 - 为每一个页面定义 Page 类,将页面的操作定义为一个个方法。
- 使用测试库实现测试用例
实现
BasePage
包装一层 WebDriver,提供常用的操作方法。
from selenium import webdriver
from selenium.webdriver.common.by import By
class BasePage:
def __init__(self, driver: webdriver.Remote):
self.driver = driver
def open(self, url: str):
"""
在当前操作窗口打开指定 url
:param url: url 字符串
"""
self.driver.get(url)
def get_element(self, locator):
"""
通过定位器查找元素列表
:param locator: 定位器字符串
"""
return self.driver.find_element(*self.__parse_locator(locator))
def get_elements(self, locator):
"""
通过定位器查找元素列表
:param locator: 定位器字符串
:return: WebElement 列表
"""
return self.driver.find_elements(*self.__parse_locator(locator))
def click(self, locator):
"""
点击指定定位器的元素
:param locator: 定位器字符串
"""
self.get_element(locator).click()
def send_keys(self, locator, *value):
"""
选中指定定位器元素然后模拟输入
:param locator: 定位器字符串
"""
self.get_element(locator).send_keys(*value)
def switch_to_frame(self, locator):
"""
切换到指定 frame
:param locator: iframe 元素定位器字符串
"""
self.driver.switch_to.frame(self.get_element(locator))
def switch_to_parent_frame(self):
"""
切换到父 frame
"""
self.driver.switch_to.parent_frame()
def switch_to_default_content(self):
"""
切换到顶层
"""
self.driver.switch_to.default_content()
def alert_accept(self):
"""
确认 alert 弹窗
"""
self.driver.switch_to.alert.accept()
def alert_dismiss(self):
"""
取消 alert 弹窗
"""
self.driver.switch_to.alert.dismiss()
def alert_send_keys(self, value: str):
"""
确认 alert 弹窗
"""
self.driver.switch_to.alert.send_keys(value)
def screenshot(self, filename):
"""
截图
:param filename: 文件路径
"""
self.driver.save_screenshot(filename)
@staticmethod
def wait(second=2):
sleep(second)
@staticmethod
def __parse_locator(locator: str) -> (By, str):
"""
解析定位器字符串
"""
selector = [s.strip() for s in locator.split(",", 1)]
if len(selector) != 2: raise Exception("错误的定位器格式")
key, selector_value = selector
match key:
case "i" | "id":
return By.ID, selector_value
case "n" | "name":
return By.NAME, selector_value
case "cls" | "class":
return By.CLASS_NAME, selector_value
case "t" | "tag":
return By.TAG_NAME, selector_value
case "l" | "link":
return By.LINK_TEXT, selector_value
case "pl" | "partial_link":
return By.PARTIAL_LINK_TEXT, selector_value
case "c" | "css":
return By.CSS_SELECTOR, selector_value
case "x" | "xpath":
return By.XPATH, selector_value
case _:
raise Exception("不支持的定位器格式")
BaiduPage
创建 BaiduPage 类,用于封装百度页面元素的定位器和相关操作。
from bases import BasePage
class BaiduPage(BasePage):
url = "https://www.baidu.com/"
search_input_locator = "i,kw"
search_button_locator = "i,su"
def search_input(self, value):
self.send_keys(self.search_input_locator, value)
def search_click(self):
self.click(self.search_button_locator)
创建测试用例
这里使用的是 pytest
from selenium import webdriver
from pages.BaiduPage import BaiduPage
def test_baidu_search():
driver = webdriver.Chrome()
page = BaiduPage(driver)
page.open(BaiduPage.url)
page.open(page.url)
page.search_input("POM")
page.search_click()
page.wait()
page.screenshot("baidu.png")
driver.quit()
这里有一个优化空间,driver 是创建在测试用例中创建的,可使用 pytest 的 fixture 创建一个公共的 WebDriver。