Selenium自动化测试入门:从环境搭建到实战封装
1. 项目概述:从手工到自动化的必然跨越
干了这么多年软件测试,我最大的感受就是,手工测试就像用勺子舀干一个游泳池,而自动化测试则是打开了排水阀。今天要聊的“Selenium”,就是那个最经典、最可靠的“排水阀”之一。无论你是刚入行的测试新人,还是想从功能测试转向自动化的老手,这个系列都将带你从零开始,亲手搭建起一套可用的自动化测试框架。我们不会空谈理论,而是聚焦于“怎么做”和“为什么这么做”,把我在实际项目中踩过的坑、总结的技巧都揉碎了讲给你听。Selenium的核心价值在于模拟真实用户操作浏览器,实现Web应用的自动化测试,它能帮你把那些重复、枯燥的回归测试任务交给机器,从而解放人力去进行更有价值的探索性测试和复杂场景设计。
2. Selenium生态与核心组件拆解
在动手写第一行代码之前,我们必须先理清Selenium到底是什么,以及它由哪些部分组成。很多人一上来就pip install selenium,然后对着报错一头雾水,问题往往就出在对整体架构的理解上。
2.1 Selenium工具集:不止一个“Selenium”
Selenium其实是一个项目集合,包含了多个工具,针对不同的需求和场景。
Selenium IDE:这是一个浏览器插件(支持Chrome和Firefox),主要用于录制和回放测试脚本。它非常适合测试新手快速上手,或者用于快速生成一些基础的操作脚本。但它的局限性也很明显:录制的脚本可维护性差,难以处理复杂逻辑(如条件判断、数据驱动),且通常绑定在特定浏览器上。在真正的项目自动化中,它更多是作为一个辅助工具,用于快速探索或生成部分操作代码。
Selenium WebDriver:这才是我们常说的“Selenium”的核心,也是本系列重点讲解的对象。WebDriver提供了一套面向各种编程语言(Java, Python, C#, JavaScript, Ruby等)的API。这些API允许你通过代码直接向浏览器发送指令(如点击、输入、获取元素),并获取浏览器的响应。它的强大之处在于,它调用的是浏览器原生支持的控制接口,因此能实现最接近真实用户的操作。我们后续的所有自动化脚本,都将基于WebDriver来编写。
Selenium Grid:这是一个用于分布式测试的工具。想象一下,你需要同时在不同操作系统(Windows, macOS, Linux)的不同浏览器(Chrome, Firefox, Safari, Edge)上运行同一套测试用例,以进行兼容性测试。手动一台台机器去跑显然不现实。Selenium Grid允许你将测试命令分发到网络中的多个节点(即不同的机器和浏览器环境)上并行执行,极大地提升了测试效率,尤其适合大型项目和持续集成流水线。
对于绝大多数从零开始的个人学习或中小项目,我们的起点和核心就是Selenium WebDriver。
2.2 WebDriver的工作原理:与浏览器对话的桥梁
理解WebDriver如何工作,有助于你在遇到诡异问题时进行排查。它的架构遵循W3C标准,可以概括为“客户端-服务器”模式。
- 客户端(Client):就是你用Python(或Java等)写的测试脚本。你调用Selenium提供的客户端库(如
from selenium import webdriver)。 - 浏览器驱动(Browser Driver):这是一个独立的可执行文件,如
chromedriver(用于Chrome/Edge)、geckodriver(用于Firefox)。它充当了服务器。你的客户端脚本通过HTTP请求(使用JSON Wire Protocol或W3C协议)向这个驱动发送命令。 - 浏览器(Browser):浏览器驱动接收到命令后,通过浏览器提供的原生自动化接口(如Chrome DevTools Protocol)来控制真实的浏览器实例执行相应操作,并将结果返回给驱动,驱动再返回给你的脚本。
所以,一个完整的Selenium自动化环境必须包含三部分:你的测试脚本 + 对应语言的Selenium客户端库 + 对应浏览器的驱动。最常见的错误就是只安装了Python的selenium包,却忘了下载或正确配置chromedriver。
2.3 与其他工具的对比:为什么是Selenium?
市面上自动化测试工具很多,比如Playwright、Cypress、Puppeteer等。Selenium的优势和定位是什么?
- Selenium vs. Playwright/Cypress:Playwright和Cypress是较新的工具,它们在架构上更现代,默认支持无头模式、自动等待、网络拦截等高级特性,开箱即用体验更好。Selenium的优势则在于其历史最久、社区最庞大、生态最成熟。几乎所有浏览器都官方支持Selenium,有海量的资料、问答和第三方库(如Page Object模式框架、报告工具)。对于需要支持老旧浏览器(如IE)或深度定制化框架的企业级项目,Selenium的灵活性和可控性更强。选择Selenium,意味着你站在了一个巨人的肩膀上,有整个开源社区作为后盾。
- Selenium for Web vs. Appium:Selenium专注于Web应用。如果你需要测试手机原生App或混合应用,那么应该使用Appium。有趣的是,Appium在底层复用了一部分Selenium的协议和思想,所以学会Selenium对学习Appium有很大帮助。
- 功能自动化 vs. 接口自动化:这是两个不同层面。Selenium属于UI层功能自动化,模拟用户操作界面。而接口自动化(常用Requests, Postman等工具)是直接测试后端API。一个完整的自动化测试体系通常两者结合:接口自动化保证业务逻辑正确、快速反馈;UI自动化保证前端交互和集成无误。在项目后期,UI自动化由于执行慢、稳定性受前端影响大,更适合作为冒烟测试或核心流程的回归保障。
3. 环境搭建与核心配置实战
理论清楚了,我们立刻动手搭建环境。这里以最流行的组合Python 3.x + Chrome浏览器 + Selenium为例。
3.1 基础环境安装
- 安装Python:确保你的系统已安装Python 3.6或更高版本。在命令行输入
python --version或python3 --version检查。 - 安装Selenium客户端库:通过pip安装,这是最简单的一步。
我建议总是使用虚拟环境(如venv或conda)来管理项目依赖,避免不同项目间的包版本冲突。pip install selenium - 下载浏览器驱动:这是最关键也最容易出错的一步。
- ChromeDriver:访问 ChromeDriver官网 或国内镜像站。驱动的版本必须与你的Chrome浏览器主版本号完全一致。查看Chrome版本:浏览器设置 -> 关于Chrome。下载对应版本的
chromedriver压缩包。 - GeckoDriver (for Firefox):访问 GeckoDriver GitHub发布页 。
- Microsoft Edge Driver:如果你的Edge是基于Chromium的,那么使用
edgedriver,其版本也需与Edge浏览器版本匹配。
- ChromeDriver:访问 ChromeDriver官网 或国内镜像站。驱动的版本必须与你的Chrome浏览器主版本号完全一致。查看Chrome版本:浏览器设置 -> 关于Chrome。下载对应版本的
3.2 驱动的配置与“找不到驱动”问题解决
下载的驱动是一个可执行文件(Windows是.exe,macOS/Linux无后缀)。有三种常用配置方法:
方法一:放入系统PATH(推荐)将下载的chromedriver或geckodriver文件,放在系统环境变量PATH包含的任意目录下,例如:
- Windows: 放在
C:\Windows\或C:\Windows\System32\。 - macOS/Linux: 放在
/usr/local/bin/。
这样做之后,你在代码中初始化驱动时,Selenium会自动在PATH中查找。
方法二:指定驱动文件路径在代码中显式指定驱动文件的绝对路径。
from selenium import webdriver # 指定驱动路径 driver = webdriver.Chrome(executable_path='/path/to/your/chromedriver') # 注意:新版本Selenium中,executable_path参数已弃用,推荐使用Service类 # 新版本推荐写法 from selenium.webdriver.chrome.service import Service service = Service(executable_path='/path/to/your/chromedriver') driver = webdriver.Chrome(service=service)方法三:使用WebDriver Manager(强烈推荐给新手)这是一个第三方库,能自动下载、匹配并管理浏览器驱动,彻底解决版本匹配的烦恼。
pip install webdriver-manager使用起来极其简单:
from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.service import Service service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service)WebDriverManager会自动检查你的浏览器版本,下载对应的驱动,并返回路径。这是我目前最推荐的方式,尤其在持续集成环境中。
注意:如果遇到驱动无法打开或秒退的问题,除了检查版本,还要注意:
- 驱动文件是否有可执行权限(Linux/macOS系统:
chmod +x chromedriver)。- 是否杀毒软件或系统安全策略拦截了驱动运行。
- 尝试以管理员/root权限运行你的脚本。
3.3 编写并运行第一个脚本:打开百度搜索
环境就绪,我们来写一个最简单的脚本,感受一下Selenium的威力。
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys import time # 1. 创建浏览器驱动实例(这里假设驱动已在PATH中,或使用WebDriver Manager) driver = webdriver.Chrome() # 这将打开一个全新的Chrome窗口 # 2. 访问目标网址 driver.get("https://www.baidu.com") # 3. 定位搜索框,并输入搜索词 # 通过元素的ID属性定位,这是最快最准的方式 search_box = driver.find_element(By.ID, "kw") search_box.send_keys("Selenium自动化测试") # 输入文字 # 4. 模拟按下回车键进行搜索 search_box.send_keys(Keys.RETURN) # 5. 等待一下,观察结果 time.sleep(3) # 强制等待3秒,这是一种简单的等待方式,但不推荐在生产脚本中使用 # 6. 可以获取当前页面的标题或URL,并打印 print("当前页面标题:", driver.title) print("当前页面URL:", driver.current_url) # 7. 关闭浏览器窗口 driver.quit() # 使用quit()会关闭所有窗口并结束驱动进程。close()只关闭当前窗口。将上面的代码保存为first_test.py,然后在命令行运行python first_test.py。你应该能看到一个Chrome窗口自动打开,访问百度,输入关键词并搜索,然后打印出信息后关闭。
实操心得:
driver.quit()和driver.close()有区别。quit()会退出整个WebDriver会话,释放资源,是结束测试时的标准操作。close()仅关闭当前标签页,如果只有一个标签页则关闭浏览器,但驱动进程可能还在。养成用driver.quit()收尾的习惯。- 上面的脚本使用了
time.sleep(3),这是一种“强制等待”。在实际项目中,应尽量避免使用,因为它会造成不必要的时间浪费,且无法智能判断页面是否就绪。我们马上会讲到更优的“智能等待”。
4. 元素定位:自动化测试的基石
自动化测试的本质是“找到元素,操作元素”。因此,元素定位的准确性和稳定性是脚本能否成功运行的关键。Selenium提供了8种主要的定位方式(通过By类)。
4.1 八大定位策略详解与选用原则
ID(
By.ID):通过元素的id属性定位。id在HTML中应该是唯一的,因此这是优先级最高、最可靠的定位方式。如果元素有稳定且唯一的ID,请毫不犹豫地使用它。element = driver.find_element(By.ID, “username”)Name(
By.NAME):通过元素的name属性定位。name也可能不唯一,但在表单元素中很常见。element = driver.find_element(By.NAME, “password”)Class Name(
By.CLASS_NAME):通过元素的class属性定位。一个元素可以有多个class(空格分隔),而一个class也可以被多个元素使用,所以稳定性较差。通常用于辅助定位或定位一组元素。elements = driver.find_elements(By.CLASS_NAME, “btn-primary”) # 注意是 find_elements,返回列表Tag Name(
By.TAG_NAME):通过标签名定位,如<input>,<div>,<a>。一个页面中同类型标签极多,几乎从不单独用于精确定位,多用于查找元素集合。all_links = driver.find_elements(By.TAG_NAME, “a”)Link Text(
By.LINK_TEXT):专门用于定位超链接 (<a>标签),通过链接的完整可见文本定位。element = driver.find_element(By.LINK_TEXT, “登录”)Partial Link Text(
By.PARTIAL_LINK_TEXT):通过链接的部分可见文本定位。比Link Text更灵活,但也要注意文本的唯一性。element = driver.find_element(By.PARTIAL_LINK_TEXT, “忘记密”)CSS Selector(
By.CSS_SELECTOR):使用CSS选择器语法定位。功能非常强大,可以组合ID、Class、属性、层级关系等进行复杂定位。是仅次于ID的推荐定位方式,尤其当元素没有ID时。# 通过ID element = driver.find_element(By.CSS_SELECTOR, “#username”) # 通过Class element = driver.find_element(By.CSS_SELECTOR, “.submit-btn”) # 组合定位:具有class='menu'的ul下的第一个li子元素 element = driver.find_element(By.CSS_SELECTOR, “ul.menu > li:first-child”)XPath(
By.XPATH):使用XML路径语言定位。功能最强大,可以遍历整个HTML文档树,实现非常灵活的定位。但表达式可能较复杂,且性能略低于CSS Selector。# 绝对路径(脆弱,不推荐) element = driver.find_element(By.XPATH, “/html/body/div[1]/form/input[1]”) # 相对路径 + 属性定位(推荐) element = driver.find_element(By.XPATH, “//input[@id=‘username’]”) # 文本内容定位 element = driver.find_element(By.XPATH, “//button[text()=‘提交’]”) # 包含文本 element = driver.find_element(By.XPATH, “//a[contains(text(), ‘下一页’)]”)
定位策略选用优先级(个人经验):
- ID > Name:唯一且稳定,首选。
- CSS Selector:灵活、性能好、语法简洁。对于现代Web应用,前端框架生成的ID可能动态变化,此时利用稳定的Class和属性组合的CSS选择器是更好的选择。
- XPath:当CSS选择器无法满足复杂逻辑时使用,例如需要根据兄弟节点、父节点文本等关系定位。尽量避免使用长的绝对路径XPath。
- Link Text / Partial Link Text:专门用于链接,简单直接。
- Class Name / Tag Name:通常不用于精确查找单个元素,多用于查找列表或结合其他方法。
4.2find_element与find_elements的区别
find_element(By.XX, “value”):返回第一个匹配到的元素(WebElement对象)。如果没找到,会抛出NoSuchElementException。find_elements(By.XX, “value”):返回一个列表,包含所有匹配到的元素。如果没找到,返回一个空列表[],不会抛出异常。
根据你的需求选择。例如,要操作一个特定的按钮,用find_element;要获取一个商品列表中的所有项,用find_elements。
4.3 定位失败常见原因与排查技巧
即使你写对了定位器,脚本也可能因为以下原因失败:
- 页面未加载完成:这是最常见的原因。你执行定位代码时,元素可能还没被渲染出来。解决方案是使用“等待”,我们下一章详细讲。
- 元素在iframe/frame内:如果目标元素位于
<iframe>或<frame>标签内,你必须先切换到对应的frame中,才能定位其中的元素。# 通过ID或索引切换到frame driver.switch_to.frame(“frame_id”) # 或者 driver.switch_to.frame(0) # 第一个frame # 操作frame内的元素... # 操作完成后切回主文档 driver.switch_to.default_content() - 元素在Shadow DOM内:一些现代前端框架(如Vue, React的某些组件)会使用Shadow DOM,普通定位方法无法直接穿透。需要使用
driver.execute_script()执行JavaScript来访问。 - 动态ID/Class:前端框架(如React, Angular)可能会生成随机的ID或Class。此时应寻找其他不变的属性,或者使用XPath/CSS Selector通过相对关系、文本内容等定位。
- 弹窗/新窗口:操作后打开了新窗口或弹窗,需要切换句柄。
# 获取当前所有窗口句柄 all_handles = driver.window_handles # 切换到新窗口(通常是最后一个) driver.switch_to.window(all_handles[-1]) - 元素不可见或不可交互:元素可能被其他元素遮挡(如弹层),或者设置了
display: none、disabled属性。需要先确保元素可见且可操作。
排查技巧:当定位失败时,不要盲目修改代码。首先,在浏览器的开发者工具(F12)中,使用Console标签尝试你的定位表达式。
- 对于CSS Selector:
document.querySelector(“你的选择器”) - 对于XPath:
$x(“你的XPath表达式”)如果能在Console中找到元素,说明定位器本身没问题,问题可能出在等待、frame或窗口切换上。
5. 等待机制:让脚本稳定运行的关键
自动化测试脚本不稳定,十有八九是“等”得不对。Selenium提供了三种等待方式,理解并正确使用它们是写出健壮脚本的核心。
5.1 强制等待 (time.sleep)
最简单粗暴的方式,让线程暂停指定的时间。
import time time.sleep(5) # 无条件等待5秒缺点:无论页面是否已经就绪,都必须等待固定时间。如果元素提前加载好,就浪费了时间;如果元素加载比预期慢,依然会失败。仅在调试或不得已的情况下使用。
5.2 隐式等待 (implicitly_wait)
在WebDriver对象的整个生命周期(或直到被改变)内设置一个全局的等待时间。当使用find_element或find_elements定位元素时,如果元素没有立即找到,WebDriver会轮询DOM一段时间(默认0.5秒检查一次),直到找到元素或超时。
driver = webdriver.Chrome() driver.implicitly_wait(10) # 设置为10秒特点与注意事项:
- 全局性:设置一次,对所有后续的
find_element操作都生效。 - 只对元素定位有效:它只作用于“查找元素”这个动作。如果元素找到了但不可点击(例如被遮挡、未启用),它不会继续等待。
- 与显式等待混用:当同时设置了隐式等待和显式等待时,实际等待时间取两者最大值。通常建议在项目中只使用一种,推荐显式等待,以避免不可预期的长时间等待。很多团队的最佳实践是禁用隐式等待(设为0),完全使用显式等待来获得更精确的控制。
5.3 显式等待 (WebDriverWait+expected_conditions)
这是最强大、最推荐的等待方式。它允许你为某个特定的条件设置等待,条件成立则立即继续执行,否则在超时后抛出异常。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 创建一个WebDriverWait对象,设置最大等待时间10秒,轮询间隔0.5秒(默认) wait = WebDriverWait(driver, 10) # 等待直到某个元素出现并可被定位 element = wait.until(EC.presence_of_element_located((By.ID, “dynamic_element”))) # 等待直到某个元素可见(不仅存在,而且display不为none,visibility不为hidden) element = wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, “.loading-message”))) # 等待直到某个元素可被点击(可见且enabled) button = wait.until(EC.element_to_be_clickable((By.NAME, “submit”))) button.click() # 等待直到某个元素从DOM中消失(例如等待加载动画消失) wait.until(EC.invisibility_of_element_located((By.ID, “spinner”))) # 等待直到页面标题包含特定文字 wait.until(EC.title_contains(“订单提交成功”)) # 等待直到某个元素包含特定文本 wait.until(EC.text_to_be_present_in_element((By.TAG_NAME, “h1”), “欢迎回来”))为什么显式等待最好?
- 精准:只为必要的条件等待,条件满足立即执行,效率高。
- 灵活:丰富的预期条件(
expected_conditions模块)覆盖了各种场景。 - 清晰:代码明确表达了“在做什么之前,需要等待什么状态”,可读性强。
- 稳定:能有效应对网络延迟、动态加载等导致的元素状态变化。
实操心得与避坑指南:
- 超时时间设置:根据网络和应用的实际情况设置,通常5-15秒。太短容易失败,太长会拖慢测试套件整体速度。
- 选择正确的预期条件:
presence_of_element_located:元素存在于DOM中即可,哪怕它不可见。适用于你后续需要操作隐藏元素的情况。visibility_of_element_located:元素不仅存在,还要可见。这是最常用的条件,因为用户通常只能与可见元素交互。element_to_be_clickable:在“可见”的基础上,还要求元素是enabled状态。用于点击操作前的最佳检查。
- 自定义等待条件:如果内置条件不满足需求,你可以用lambda函数自定义。
# 等待直到元素的某个属性值变为特定值 wait.until(lambda d: d.find_element(By.ID, “progress”).get_attribute(“value”) == “100”) - 处理超时异常:
until方法在超时后会抛出TimeoutException。你应该在测试框架中妥善捕获并处理这个异常,将其转化为测试失败并输出有意义的日志,而不是让整个脚本崩溃。 - 避免“等待链”:不要在循环或连续操作中频繁使用短时间的显式等待,这可能导致累积等待时间很长。尽量用一个等待条件覆盖一个完整的交互阶段。
6. 常用浏览器操作与高级交互
掌握了定位和等待,我们就可以组合它们来完成各种复杂的用户操作了。
6.1 基础操作:点击、输入、清空、提交
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 假设driver已初始化 wait = WebDriverWait(driver, 10) # 1. 点击操作 login_button = wait.until(EC.element_to_be_clickable((By.ID, “loginBtn”))) login_button.click() # 2. 输入文本 username_input = driver.find_element(By.ID, “username”) username_input.send_keys(“my_username”) # 输入 username_input.clear() # 清空输入框 username_input.send_keys(“new_username”) # 重新输入 # 3. 模拟特殊键 search_input = driver.find_element(By.NAME, “q”) search_input.send_keys(“Selenium”) search_input.send_keys(Keys.ENTER) # 模拟回车键,等同于 .send_keys(Keys.RETURN) # 也可以组合键,如Ctrl+A全选 search_input.send_keys(Keys.CONTROL, ‘a’) # Windows/Linux # 对于Mac,通常是 COMMAND 键,但WebDriver可能仍映射为CONTROL,需注意系统差异 # 4. 提交表单 # 如果输入框在一个<form>里,可以通过submit()提交 form_element = driver.find_element(By.TAG_NAME, “form”) form_element.submit() # 或者直接找到提交按钮点击6.2 获取元素信息与状态判断
自动化测试不仅是操作,也需要验证。我们需要从页面上获取信息来进行断言。
element = driver.find_element(By.ID, “someElement”) # 获取文本内容(可见文本) text = element.text print(f”元素文本:{text}”) # 获取属性值 attr_value = element.get_attribute(“class”) print(f”class属性:{attr_value}”) # 对于自定义属性或标准属性都适用 data_id = element.get_attribute(“data-testid”) # 获取CSS属性值 css_color = element.value_of_css_property(“color”) print(f”字体颜色:{css_color}”) # 判断元素状态 is_displayed = element.is_displayed() # 是否可见 is_enabled = element.is_enabled() # 是否可用(未被disabled) is_selected = element.is_selected() # 是否被选中(用于复选框、单选框) # 获取元素位置和大小 location = element.location # {‘x’: 100, ‘y’: 200} size = element.size # {‘height’: 30, ‘width’: 120}6.3 高级交互:鼠标动作与键盘动作链
对于拖放、悬停、右键菜单等复杂操作,需要使用ActionChains类。
from selenium.webdriver.common.action_chains import ActionChains actions = ActionChains(driver) # 鼠标悬停 menu_element = driver.find_element(By.CSS_SELECTOR, “.dropdown-menu”) actions.move_to_element(menu_element).perform() # 注意:.perform() 是执行动作链的必须调用 # 拖放操作 source_element = driver.find_element(By.ID, “draggable”) target_element = driver.find_element(By.ID, “droppable”) actions.drag_and_drop(source_element, target_element).perform() # 或者更精确的控制 # actions.click_and_hold(source_element).move_to_element(target_element).release().perform() # 右键点击(上下文菜单) actions.context_click(element).perform() # 双击 actions.double_click(element).perform() # 组合动作:点击并按住,移动到某点,然后释放 actions.click_and_hold(element).move_by_offset(100, 50).release().perform()6.4 执行JavaScript
当Selenium API无法直接实现某些操作时(如滚动到元素、修改元素属性、处理Shadow DOM),可以借助执行JavaScript的能力。
# 执行简单的JS driver.execute_script(“alert(‘Hello Selenium!’);”) # 带参数的JS,并获取返回值 title = driver.execute_script(“return document.title;”) print(title) # 滚动到元素可见(非常实用的操作) element = driver.find_element(By.ID, “footer”) driver.execute_script(“arguments[0].scrollIntoView(true);”, element) # true表示与窗口顶部对齐 # 修改元素属性(常用于调试或处理特殊场景) driver.execute_script(“arguments[0].setAttribute(‘style’, ‘border: 2px solid red;’);”, element) # 处理Shadow DOM(示例) # 假设有一个自定义组件 <my-component>,内部有Shadow Root shadow_host = driver.find_element(By.TAG_NAME, “my-component”) shadow_root = driver.execute_script(“return arguments[0].shadowRoot”, shadow_host) # 现在可以通过shadow_root来查找内部元素(需要再次执行JS或使用其他方法,Selenium 4有改进支持) inner_element = driver.execute_script(“return arguments[0].querySelector(‘.inner-button’)”, shadow_root) inner_element.click()注意事项:虽然execute_script很强大,但过度使用会使测试脚本与前端实现细节(JS)耦合过紧,降低可维护性。应优先使用标准的WebDriver API。
7. 实战:封装一个简单的页面操作类
将上述知识结合起来,我们尝试为一个简单的登录页面封装一个操作类。这是迈向Page Object模式(后续文章会详述)的第一步。
假设我们有一个登录页面,包含用户名输入框、密码输入框、登录按钮和错误提示区域。
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: “”“登录页面操作封装”“” # 页面元素定位器(Locators),集中管理,便于维护 USERNAME_INPUT = (By.ID, “username”) PASSWORD_INPUT = (By.ID, “password”) LOGIN_BUTTON = (By.CSS_SELECTOR, “button[type=‘submit’]”) ERROR_MESSAGE = (By.CLASS_NAME, “alert-error”) def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def navigate_to(self, url): “”“导航到登录页面”“” self.driver.get(url) # 可以在这里增加一个等待,确保页面关键元素加载完成 self.wait.until(EC.presence_of_element_located(self.USERNAME_INPUT)) return self def enter_username(self, username): “”“输入用户名”“” username_elem = self.wait.until(EC.visibility_of_element_located(self.USERNAME_INPUT)) username_elem.clear() username_elem.send_keys(username) return self # 支持链式调用 def enter_password(self, password): “”“输入密码”“” password_elem = self.driver.find_element(*self.PASSWORD_INPUT) # 因为密码框通常就在用户名下面,可以不用再等待 password_elem.clear() password_elem.send_keys(password) return self def click_login(self): “”“点击登录按钮”“” login_btn = self.wait.until(EC.element_to_be_clickable(self.LOGIN_BUTTON)) login_btn.click() return self def get_error_message(self): “”“获取错误提示信息,如果存在的话”“” try: # 错误信息可能不会立即出现,等待一小会儿 error_elem = WebDriverWait(self.driver, 3).until( EC.visibility_of_element_located(self.ERROR_MESSAGE) ) return error_elem.text except: # 如果没有找到错误信息元素,返回None或空字符串 return None def login(self, username, password): “”“完整的登录流程”“” self.enter_username(username) self.enter_password(password) self.click_login() # 登录后,可以返回下一个页面的对象,这里简单返回自身 # 实际项目中,这里可能会 return HomePage(self.driver) return self # 使用示例 if __name__ == “__main__”: from selenium import webdriver driver = webdriver.Chrome() try: login_page = LoginPage(driver) login_page.navigate_to(“http://example.com/login”) # 测试登录失败场景 login_page.login(“wrong_user”, “wrong_pass”) error_msg = login_page.get_error_message() if error_msg: print(f”登录失败,错误信息:{error_msg}”) else: print(“未捕获到预期的错误信息”) # 可以继续测试登录成功场景... # login_page.login(“correct_user”, “correct_pass”) # 验证是否跳转到首页... finally: driver.quit()这个简单的封装带来了几个好处:
- 代码复用:登录操作被封装成一个方法,多个测试用例都可以调用。
- 易于维护:所有元素定位器集中在类顶部,如果页面元素ID变了,只需修改一处。
- 可读性高:测试用例读起来像自然语言:
login_page.login(“user”, “pass”)。 - 减少重复:等待逻辑被封装在方法内部,调用者无需关心。
这就是Page Object Model (POM) 设计模式的雏形。在后续文章中,我们会深入探讨如何构建更健壮、可扩展的POM框架,并集成单元测试框架(如pytest)来组织和管理测试用例。
