Skip to content

Selenium

Selenium 是支持 Web 浏览器自动化的一系列工具和库的 开源免费 综合项目。

支持模拟用户与浏览器的交互,它实现了 W3C WebDriver 规范的基础结构。

  • 简洁的面向对象 API。
  • 有效地驱动浏览器。

基本使用流程就是下载浏览器的对应版本的驱动程序并添加到 PATH 环境 中,然后使用自己擅长的开发语言编写自动化测试代码。

支持语言:JavaPythonCSharpRubyJavaScriptKotlin(Java)

常用类属性和方法

WebDriver

get

打开一个浏览器页面,参数是一个 URL 地址。

python
# 定义
def get(self, url: str) -> None
    pass

driver.get("https://www.baidu.com")

find_element & find_elements

通过制定的元素定位方式查找页面元素,前者是指返回第一个匹配的元素,后者返回所有匹配元素的列表。

python
# 定义
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 页面,可通过参数配置,默认由浏览器自动选择。

python
# 打开一个 tab 页
driver.switch_to.new_window("tab")
# 打开一个新的窗口
driver.switch_to.new_window("window")
frame

切换到指定的 frame。

python
# 切换到指定 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)

等价于:

js
function execute() {
    // script
    // 可通过 arguments[index] 访问传入的参数
}

execute(...args)

WebElement

send_keys

模拟在元素中打字,对于 file input 还可以直接指定本地文件路径进行文件选择。

click

单击该元素。

元素定位

Selenium 支持 8 种基本的元素定位方式和 5 中相对定位方式。

基本元素定位

这八种元素定位方式分别是 IDNAMETAG_NAMECLASS_NAMELINK_TEXTPARTIAL_LINK_TEXTCSS_SELECTORXPATH

ID

通过元素的唯一 ID 进行定位。

python
driver.find_element(By.ID, "kw")

NAME

通过元素的 name 属性进行定位。

TAG_NAME

通过元素的标签名进行定位。

CLASS_NAME

通过元素 class 属性进行定位。

python
driver.find_element(By.CLASS_NAME, "class1")
# 多个类名需要使用 . 拼接
driver.find_element(By.CLASS_NAME, "class1.class2")

通过超链接的文本进行匹配。

通过超链接的部分文本进行匹配。

CSS_SELECTOR

通过 CSS 选择器进行匹配。

菜鸟教程 - CSS 选择器

XPATH

通过 XPATH 进行匹配。

  • 优点:能定位到任何元素。
  • 缺点:执行效率低。

XPath - W3 School

相对定位

这五种相对定位分别是 abovebelowto_left_ofto_right_ofnear

  • above:查找在指定元素上方的元素。
  • below:查找在指定元素下方的元素。
  • to_left_of:查找在指定元素左边的元素。
  • to_right_of:查找在指定元素右边的元素。
  • near:在在指定元素周围的元素。
示例

这里使用本地 HTML 文件进行测试:

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 方便测试)

python
# 查找在密码输入框上面的邮箱输入框
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 属性值可见文本选择。
  • 支持通过上述三种方式取消选择,同时也支持取消全部选择。
python
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_toalert 属性进行处理。

python
driver.switch_to.alert.accept()  # 确定
driver.switch_to.alert.dismiss()  # 取消
driver.switch_to.alert.send_keys()  # 输入 prompt 弹窗内容

鼠标和键盘事件(ActionChains)

ActionChains 用于配置一个操作执行链,通过 perform 执行。

点击

python
ActionChains(driver).click(element).perform()

双击

python
ActionChains(driver).double_click(element).perform()

右键

python
ActionChains(driver).context_click(element).perform()

按住键盘某个键并操作

python
from selenium.webdriver.common.keys import Keys

ActionChains.key_down(Keys.CONTROL).click(element).key_up(Keys.CONTROL).perform()

移动到元素(悬浮)

python
ActionChains(driver).move_to_element(element).perform()

键盘操作

python
from selenium.webdriver.common.keys import Keys

ActionChains(driver).send_keys(Keys.ENTER).perform()

等待方式

sleep

使用 time 模块下的 sleep 进行固定的等待时间。

python
from time import sleep

# 休眠两秒
sleep(2)

隐式等待

调用 WebDriver.implicitly_wait 方法设置默认的隐式等待时间。

python
# 隐式等待 2s,2s 内找到了元素会提前返回。
driver.implicitly_wait(2)

显式等待

使用 WebDriverWait 类实现显式等待。

python
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)

浏览器相关

主流浏览器驱动下载

浏览器下载地址扩展选项
ChromeChrome >= 115
Chrome < 115
ChromeOptions
EdgeWeb DriverEdgeOptions
FirefoxgeckodriverfirefoxOptions

浏览器配置

无头模式

没有图形用户界面 (GUI) 的 Web 浏览器。

⚠️ 限制

使用无头模式对浏览器窗口大小的操作时将无效,即无法最大化窗口。

Chromium

通过添加启动选项 --headless 开启。

版本要求:>= 59

Chrome 推出新的无头模式

官方说明:Chrome 的无头模式升级了:推出 --headless=new

Chrome 112 提供新的无头模式,在此模式下,Chrome 会创建但不显示任何平台窗口。所有其他功能(包括现有和未来的功能)均不受限制。

需要配置启动选项为 new 即 --headless=new

浏览器启动窗口大小

Chrome

通过添加启动选项 --window-size=1920,1080 进行配置。

使得 JS 无法检查

Chrome
python
# 关闭 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 中的默认字符串为:

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"

自定义映射规则:

自定义符号定位器
iidid
clsclassclass name
nnamename
ttagtag name
llinklink text
plparital_linkpartial link text
ccsscss selector
xxpathxpath
python
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 分层

  1. 表现层:页面的可见元素。
  2. 操作层:对页面上的元素进行操作。
  3. 业务层:执行测试的详细步骤。

核心

  1. 定义 BasePage 用来封装 Selenium 的基本操作。
  2. 为每一个页面定义 Page 类,将页面的操作定义为一个个方法。
  3. 使用测试库实现测试用例

实现

BasePage

包装一层 WebDriver,提供常用的操作方法。

python
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 类,用于封装百度页面元素的定位器和相关操作。

python
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

python
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。