Python Selenium自动化测试实战:从环境搭建到POM框架
1. 项目概述:从“点点点”到“自动化”的质变
干了这么多年测试,最烦的就是那些重复的、机械的网页操作。比如每天上班第一件事,就是登录后台系统,导出昨天的数据报表,再手动填到另一个系统里生成图表。一次两次还行,日复一日,不仅枯燥,还容易因为手滑点错。后来我开始接触自动化测试,尤其是用 Python 的 Selenium 库,感觉就像给枯燥的“点点点”工作装上了机械臂。它不是什么高深莫测的黑科技,本质上就是一个能按照你写好的脚本,去模拟真人操作浏览器(点击、输入、滚动等)的工具。今天,我就结合自己踩过的无数坑,来聊聊怎么用 Python + Selenium 把这套“机械臂”玩转,让你从重复劳动中彻底解放出来。
简单说,Selenium 就是一个浏览器自动化工具。你写一段 Python 代码,告诉它:“打开 Chrome,访问某个网址,在用户名框里输入‘admin’,在密码框里输入‘123456’,然后点击登录按钮。” 它就能一丝不苟地执行。这不仅能用于测试网页功能是否正常(这就是自动化测试的核心),还能用来做一些日常的、规则明确的网页数据抓取或操作任务,比如定时签到、批量下载、数据填报等。无论你是测试工程师、数据分析师,还是任何需要和网页打交道的开发者,掌握它都能极大提升效率。接下来,我会从环境搭建、核心操作、实战技巧到避坑指南,带你完整走一遍。
2. 环境准备与核心工具选型
工欲善其事,必先利其器。在开始写自动化脚本之前,一套稳定、匹配的环境是成功的基石。这里面的坑,我几乎一个没落全踩过。
2.1 Python 环境搭建:别在版本上栽跟头
首先肯定是 Python。我的建议是, 直接安装 Python 3.7 到 3.11 之间的版本 。太老的版本(如 2.7)库支持不好,太新的版本(如 3.12+)有时会遇到一些第三方库兼容性问题。去 Python 官网下载安装包时,务必勾选 “Add Python to PATH” 这个选项,这是为了能在命令行里直接使用 python 和 pip 命令。
安装完成后,打开命令行(CMD 或 Terminal),输入 python --version 检查是否安装成功。接下来,我们需要一个写代码的地方。对于新手,我强烈推荐 Visual Studio Code (VSCode) 。它轻量、免费,而且通过安装插件能获得非常好的 Python 开发体验。在 VSCode 中,你需要安装官方的 “Python” 扩展,它会帮你管理 Python 解释器、提供代码提示、调试等功能。配置环境时,在 VSCode 左下角选择你刚安装的 Python 解释器,这样就完成了最基本的编码环境搭建。
注意:很多教程会教你在系统环境变量里手动配置 PATH,如果你在安装时忘了勾选,确实需要手动加一下。但更常见的问题是,电脑里装了多个 Python(比如 Anaconda 自带一个,官网又装一个),导致命令混乱。这时候,在 VSCode 里明确指定使用哪一个解释器至关重要。
2.2 Selenium 库安装与浏览器驱动配置
这是新手最容易卡住的地方。Selenium 分为两部分: Python 客户端库 和 浏览器驱动 。
第一步,安装 Selenium 库。 这个很简单,在命令行里执行:
pip install selenium
如果速度慢,可以使用国内镜像源,例如:
pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple
第二步,下载浏览器驱动。 Selenium 本身不能直接控制浏览器,它需要通过一个名为 “WebDriver” 的中间件来发送指令。你需要根据你使用的浏览器类型和版本,下载对应的驱动。
- Chrome 浏览器 :驱动叫
ChromeDriver。去 ChromeDriver 官网 下载。 关键点来了:驱动版本必须与你的 Chrome 浏览器主版本号完全一致! 你可以在 Chrome 的“设置”->“关于 Chrome”里查看版本号(比如 120.0.6099.110),然后下载版本号为 120 的 ChromeDriver。 - Firefox 浏览器 :驱动叫
geckodriver。去 GitHub 发布页 下载。 - Microsoft Edge 浏览器 :驱动叫
msedgedriver。去 Microsoft Edge 开发者网站 下载。同样需要版本匹配。
驱动下载下来是一个可执行文件(如 chromedriver.exe 或 chromedriver )。接下来有三种配置方式:
- (推荐)放在 Python 脚本目录下 :将驱动文件直接复制到你的项目文件夹里。这样在代码中指定路径最简单。
- 放在系统 PATH 环境变量包含的目录里 :比如 Windows 的
C:\Windows\System32。这样你可以在代码中直接使用webdriver.Chrome()而不指定路径,但管理多个版本时容易混乱。 - 在代码中指定绝对路径 :最明确,但代码移植性差。
我个人的习惯是 方法1 ,为每个项目单独管理驱动,清晰无污染。
2.3 可选工具:为什么我推荐 Playwright 作为进阶了解
在你熟练使用 Selenium 之后,可能会听到另一个工具: Playwright 。它是微软推出的新一代浏览器自动化库。这里简单对比一下,帮你建立认知:
- Selenium :老牌、稳定、社区庞大、支持语言多(Java, Python, C#等)、支持浏览器多。但速度相对较慢,API 设计有些历史包袱,处理现代单页面应用(SPA)的异步加载有时需要写很多“等待”代码。
- Playwright :后起之秀,默认支持无头模式且速度快,API 设计更现代优雅,自动等待机制(Auto-waiting)极大地减少了手动编写等待时间的代码,对 SPA 支持更好。但相对较新,某些极端场景的社区解决方案可能不如 Selenium 丰富。
对于初学者, 从 Selenium 开始绝对没错 ,它的资料最多,遇到的问题基本都能搜到答案。但了解 Playwright 的存在和优势,能让你在未来技术选型时多一个更优的选择。本次我们聚焦 Selenium。
3. 核心操作解析:模拟人的每一步
环境配好了,我们来拆解一个真人操作网页的过程,并用代码一一对应实现。假设我们要自动化登录一个假设的管理后台。
3.1 启动浏览器与访问页面
这是所有操作的起点。
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
# 1. 创建浏览器驱动实例
# 如果你的 chromedriver.exe 放在项目根目录
driver = webdriver.Chrome()
# 或者指定路径
# driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')
# 2. 访问目标网址
driver.get("https://example.com/admin/login")
# 3. 最大化窗口(可选,更接近人工操作)
driver.maximize_window()
time.sleep(2) # 等待2秒,让页面加载一下。这是初级做法,后面我们会改进。
webdriver.Chrome() 这行代码会打开一个全新的、干净的 Chrome 浏览器窗口。 driver.get() 相当于你在地址栏输入网址并回车。
3.2 元素定位:自动化测试的“眼睛”
要让程序在页面上找到输入框、按钮,你必须告诉它“在哪里”。这就是元素定位。Selenium 提供了8种主要的定位方式,常用的是前几种:
| 定位方式 | 代码示例 (By.XXX) | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| ID | By.ID, “username” |
元素有唯一id属性时 | 定位最快、最精确 | 不是所有元素都有id |
| Name | By.NAME, “password” |
表单元素常用 | 也比较精确 | 同ID,可能不唯一 |
| Class Name | By.CLASS_NAME, “btn-submit” |
通过CSS类名定位 | 常用 | 类名经常不唯一 |
| Tag Name | By.TAG_NAME, “input” |
定位特定类型的标签 | 简单 | 通常返回多个元素 |
| Link Text | By.LINK_TEXT, “忘记密码?” |
定位纯文本链接 | 精确 | 只用于 <a> 标签 |
| Partial Link Text | By.PARTIAL_LINK_TEXT, “忘记” |
链接文本部分匹配 | 更灵活 | 可能匹配多个 |
| XPath | By.XPATH, ‘//*[@id=“loginForm”]/input[1]’ |
万能定位法,可遍历DOM | 功能最强大 | 表达式复杂,性能稍差 |
| CSS Selector | By.CSS_SELECTOR, ‘#loginForm .btn’ |
类似CSS选择器 | 语法简洁,性能好 | 学习CSS选择器语法 |
实操心得 :
- 优先级 : ID > Name > CSS Selector > XPath 。ID 和 Name 是首选,因为它们通常由开发人员赋予业务含义且唯一。如果都没有,优先学习使用 CSS Selector,它的性能通常优于复杂的 XPath,且语法更简洁。
- 如何获取元素属性 :在浏览器中按 F12 打开开发者工具,使用左上角的箭头工具点击页面元素,就能在 Elements 面板看到其 HTML 代码和 id、name、class 等属性。
- XPath 和 CSS Selector 技巧 :
- XPath :
//表示从根节点开始查找,input[@name=‘user’]表示查找 name 属性为 ‘user’ 的 input 标签。可以用and、or组合条件。 - CSS Selector :
#id表示 id,.class表示 class,input[type=‘text’]表示属性选择。>表示直接子元素,空格表示后代元素。
- XPath :
3.3 元素操作:模拟手和键盘
找到元素后,就可以操作了。最常用的三种操作:点击、输入、获取信息。
# 假设页面有 <input id="username">, <input id="password">, <button id="loginBtn">
# 定位元素
username_input = driver.find_element(By.ID, "username")
password_input = driver.find_element(By.ID, "password")
login_button = driver.find_element(By.ID, "loginBtn")
# 1. 输入文本 - 模拟打字
username_input.send_keys("admin")
password_input.send_keys("secure_password_123")
# 2. 点击元素 - 模拟鼠标点击
login_button.click()
# 3. 获取元素信息 - 用于断言或记录
print(f"登录按钮的文本是:{login_button.text}")
print(f"用户名输入框的占位符是:{username_input.get_attribute('placeholder')}")
# 获取输入框内的值(对于某些动态生成的输入框)
# value = username_input.get_attribute('value')
send_keys() 不仅可以输入普通文本,还可以模拟快捷键,例如 send_keys(Keys.CONTROL, ‘a’) 表示全选(Ctrl+A)。 click() 方法很简单,但后面会讲到,不是所有时候都能直接点。
3.4 等待机制:让脚本“聪明”地等
这是 Selenium 自动化中最核心、也最容易出问题的一环。页面加载需要时间,元素出现需要时间。你不能让脚本在元素还没出现时就进行操作,否则会抛出 NoSuchElementException 。
三种等待方式:
- 强制等待 (time.sleep) :上面用过的
time.sleep(2)。这是“死等”,无论页面是否加载完,都固定等2秒。 不推荐在正式脚本中使用 ,效率极低,且时间很难设定准确。 - 隐式等待 (Implicit Wait) :设置一个全局的等待时间。在这个时间内,如果元素没找到,WebDriver 会每隔一段时间重试查找,直到找到或超时。
它针对所有的driver.implicitly_wait(10) # 设置隐式等待为10秒 element = driver.find_element(By.ID, “dynamic-element”)find_element操作。缺点是它不关心元素是否“可点击”或“可见”,只关心是否存在在DOM中。对于需要等待元素具备某种状态(如可点击)的场景,可能不够。 - 显式等待 (Explicit Wait) : 这是最佳实践! 它为某个特定元素和条件设置等待。你可以指定最长等待时间,以及检查的条件(如元素可见、可点击、存在等)。WebDriver 会轮询检查条件是否成立,成立则继续,超时则报错。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待最多10秒,直到登录按钮变得可点击 wait = WebDriverWait(driver, 10) login_button = wait.until(EC.element_to_be_clickable((By.ID, "loginBtn"))) login_button.click() # 等待某个包含成功信息的元素出现 success_msg = wait.until(EC.presence_of_element_located((By.CLASS_NAME, "alert-success"))) print(success_msg.text)expected_conditions模块提供了很多条件,如visibility_of_element_located(元素可见)、text_to_be_present_in_element(元素包含特定文本)等。
我的经验 : 在项目中混合使用隐式和显式等待 。设置一个较短的全局隐式等待(如5秒),作为兜底。在关键操作点(如点击按钮后页面跳转、等待弹窗出现)使用显式等待,并指定明确的条件。彻底避免使用 time.sleep 。
4. 实战进阶:处理复杂场景与封装技巧
掌握了基本操作,我们来看看实际项目中那些让人头疼的“拦路虎”。
4.1 处理弹窗、Alert 和 新窗口/标签页
JavaScript Alert/Confirm/Prompt :
from selenium.webdriver.common.alert import Alert
# 触发一个alert后,切换到alert对象
alert = Alert(driver)
print(alert.text) # 获取alert文本
alert.accept() # 点击“确定”
# alert.dismiss() # 点击“取消”
# alert.send_keys(“输入文本”) # 针对prompt
新窗口/标签页 :
# 点击一个打开新标签页的链接前,记录当前窗口句柄
main_window = driver.current_window_handle
# 点击链接...
driver.find_element(By.LINK_TEXT, “新窗口”).click()
# 获取所有窗口句柄
all_windows = driver.window_handles
# 切换到新窗口
for window in all_windows:
if window != main_window:
driver.switch_to.window(window)
break
# 在新窗口操作...
# 操作完毕后,切回主窗口
driver.switch_to.window(main_window)
iframe 嵌套页面 : 如果元素位于 <iframe> 或 <frame> 标签内,你必须先切换到对应的 frame 中,才能定位其中的元素。
# 通过id或name切换
driver.switch_to.frame(“frame_name_or_id”)
# 或者通过定位到的frame元素切换
frame_element = driver.find_element(By.CSS_SELECTOR, “iframe.some-class”)
driver.switch_to.frame(frame_element)
# 在frame内操作元素...
# 操作完成后,切回主文档
driver.switch_to.default_content()
4.2 模拟高级用户交互:鼠标悬停、拖拽、键盘操作
需要导入 ActionChains 类来模拟复杂的鼠标和键盘操作。
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
# 鼠标悬停
menu = driver.find_element(By.ID, “dropdownMenu”)
ActionChains(driver).move_to_element(menu).perform()
# 此时,悬停触发的子菜单应该出现了,再定位子菜单项点击
sub_menu = driver.find_element(By.LINK_TEXT, “选项一”)
sub_menu.click()
# 拖拽元素
source = driver.find_element(By.ID, “draggable”)
target = driver.find_element(By.ID, “droppable”)
ActionChains(driver).drag_and_drop(source, target).perform()
# 组合按键(例如Ctrl+C复制)
ActionChains(driver).key_down(Keys.CONTROL).send_keys(‘c’).key_up(Keys.CONTROL).perform()
4.3 文件上传与下载
文件上传 :如果网页上传按钮是 <input type=“file”> ,那非常简单,直接 send_keys 文件路径即可。
upload_element = driver.find_element(By.XPATH, “//input[@type=‘file’]”)
upload_element.send_keys(“/Users/yourname/Desktop/test_image.jpg”) # 绝对路径
如果网页用了自定义的上传组件(通常是用 div 模拟的),可能需要用 ActionChains 模拟点击,或者借助 pyautogui 库模拟系统级键盘操作(不推荐,兼容性差),或者与开发沟通能否在测试环境提供原生 input。
文件下载 :需要设置浏览器选项,指定下载路径,并禁用下载弹窗。
from selenium import webdriver
options = webdriver.ChromeOptions()
prefs = {
“download.default_directory”: r“C:\Downloads”, # 指定下载路径
“download.prompt_for_download”: False, # 禁止下载弹窗
“download.directory_upgrade”: True,
“safebrowsing.enabled”: True
}
options.add_experimental_option(“prefs”, prefs)
driver = webdriver.Chrome(options=options)
之后,点击下载链接,文件就会自动保存到指定目录。
4.4 测试框架雏形:Page Object Model (POM)
当你的自动化脚本越来越多,你会发现大量重复的定位符和操作代码散落在各个测试用例里,难以维护。这时就需要 Page Object Model (页面对象模型) 。它的核心思想是 将页面封装成类,页面的元素定位和基本操作作为类的方法,测试用例只关心业务逻辑 。
举个例子,对于登录页面:
# pages/login_page.py
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:
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
# 定位器 (Locators)
USERNAME_INPUT = (By.ID, “username”)
PASSWORD_INPUT = (By.ID, “password”)
LOGIN_BUTTON = (By.ID, “loginBtn”)
ERROR_MSG = (By.CLASS_NAME, “alert-danger”)
# 页面操作方法
def enter_username(self, username):
user_elem = self.wait.until(EC.visibility_of_element_located(self.USERNAME_INPUT))
user_elem.clear()
user_elem.send_keys(username)
def enter_password(self, password):
self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)
def click_login(self):
self.driver.find_element(*self.LOGIN_BUTTON).click()
def get_error_message(self):
try:
return self.driver.find_element(*self.ERROR_MSG).text
except:
return None
# 业务场景组合方法
def login(self, username, password):
self.enter_username(username)
self.enter_password(password)
self.click_login()
然后在测试用例中,代码会变得非常清晰:
# tests/test_login.py
import pytest
from pages.login_page import LoginPage
def test_valid_login(driver): # 假设driver通过fixture注入
login_page = LoginPage(driver)
login_page.login(“admin”, “correct_password”)
# 断言登录成功,例如跳转到首页
assert “dashboard” in driver.current_url
def test_invalid_login(driver):
login_page = LoginPage(driver)
login_page.login(“wrong_user”, “wrong_pass”)
error_text = login_page.get_error_message()
assert error_text is not None
assert “用户名或密码错误” in error_text
POM 极大地提高了代码的可读性、可维护性和复用性。元素定位符只在一个地方(Page类)维护,如果页面元素变了,你只需要修改这一个文件。
5. 常见问题排查与性能优化
即使按照最佳实践来写,脚本运行时还是会遇到各种稀奇古怪的问题。这里记录一些高频问题和解决思路。
5.1 元素定位失败:NoSuchElementException
这是最最常见的错误。原因和排查步骤:
- 等待不足 :元素还没加载出来就去定位。 解决方案 :使用显式等待 (
WebDriverWait+EC)。 - 定位器写错了 :ID/Class 名称有误,或者元素在 iframe/Shadow DOM 里。 排查 :用浏览器开发者工具仔细核对定位器,检查元素是否在 iframe 内。
- 页面结构动态变化 :元素的 ID 或 Class 是随机生成的。 解决方案 :使用相对稳定的属性来定位,比如
name,或者使用包含部分文本的 XPath (//button[contains(text(), ‘提交’)]) 或 CSS 选择器 (button[type*=‘submit’])。 - 页面有多个匹配元素 :你的定位器找到了多个元素,但
find_element只返回第一个,可能不是你想要的那个。 解决方案 :使用find_elements获取列表,或者优化定位器使其唯一。也可以使用find_element并指定索引(不推荐,容易失效)。
5.2 元素不可交互:ElementNotInteractableException
找到了元素,但点击或输入时失败。可能原因:
- 元素被遮挡 :被另一个元素(如弹窗、遮罩层)盖住了。 解决方案 :等待遮挡物消失,或者用
ActionChains尝试点击。 - 元素不可见 :元素在屏幕外,或者
style=“display: none;”。 解决方案 :使用EC.visibility_of_element_located等待其可见,或者用 JavaScript 滚动到元素所在位置driver.execute_script(“arguments[0].scrollIntoView();”, element)。 - 元素是 disabled 状态 :按钮有
disabled属性。 解决方案 :等待其变为可用 (EC.element_to_be_clickable),或者检查前置操作是否未完成。
5.3 脚本运行不稳定(Flaky Tests)
有时成功有时失败,令人抓狂。除了上述等待问题,还有:
- 网络或环境不稳定 :这不是脚本问题。可以考虑在关键步骤加入重试机制,或者优化测试环境。
- 浏览器版本与驱动不匹配 : 严格检查并匹配版本!
- 使用了不稳定的定位器 :依赖于绝对路径的 XPath 或容易变化的 Class。 尽量使用有业务含义的、稳定的属性 ,如
data-testid(如果开发配合添加)。 - 异步操作未完成 :现代网页大量使用 Ajax 和前端框架,一个操作触发后,页面状态是逐步更新的。 不要只等待一个元素,要等待代表操作完成的状态出现 。例如,点击提交后,不要只等页面跳转,可以等待“提交成功”的提示框出现。
5.4 性能与稳定性优化建议
- 使用无头模式 (Headless) :在不需要观察浏览器界面的场景(如 CI/CD 流水线中),使用无头模式可以节省大量资源,运行更快。
from selenium import webdriver options = webdriver.ChromeOptions() options.add_argument(“--headless”) # 开启无头模式 options.add_argument(“--disable-gpu”) # 禁用GPU,某些系统需要 options.add_argument(“--no-sandbox”) # Linux环境下可能需要 driver = webdriver.Chrome(options=options) - 合理设置等待时间 :隐式等待不要设得太长(建议5-10秒),显式等待根据操作复杂度设置(通常10-20秒)。过长的等待会拖慢整个套件的执行速度。
- 复用浏览器实例 :对于一组相关的测试用例,可以考虑复用同一个
driver实例,而不是每个用例都开启关闭一次浏览器。这可以通过 pytest 的fixture设置scope=“session”或“module”来实现。但要注意用例间的状态隔离,避免相互影响。 - 清理 Cookies 和缓存 :对于需要登录状态的测试,在用例开始前或结束后清理 Cookies,可以保证测试的独立性。
driver.delete_all_cookies() driver.execute_script(“window.localStorage.clear();”) # 清理本地存储 - 截图和日志 :在关键步骤,特别是断言失败或发生异常时,自动截图并保存日志,对于后期排查问题有巨大帮助。
try: # 某些操作 except Exception as e: timestamp = time.strftime(“%Y%m%d_%H%M%S”) driver.save_screenshot(f“error_{timestamp}.png”) print(f“操作失败: {e}”) raise
自动化测试脚本的编写,三分在技术,七分在经验和耐心。每一个稳定运行的脚本背后,都是对业务页面交互逻辑的深刻理解和对各种边界情况的充分考虑。从最简单的登录操作开始,逐步扩展到处理复杂流程、封装页面对象、集成到持续集成平台,这条路没有捷径,但每一步都算数。当你看到原本需要手动执行一小时的回归测试,现在只需点一下按钮就能在十分钟内完成并生成报告时,那种成就感会让你觉得所有的折腾都是值得的。
更多推荐
所有评论(0)