Selenium
简介
Selenium 是一个强大的 Web 自动化测试工具,支持多种浏览器和操作系统。它主要用于:
- Web 应用自动化测试:模拟用户操作进行功能测试
- 浏览器自动化:自动化重复性 Web 任务
- Web 爬虫:动态内容抓取
- 回归测试:集成到 CI/CD 流程
核心组件:
- Selenium WebDriver:直接控制浏览器
- Selenium IDE:浏览器录制和回放工具
- Selenium Grid:分布式测试执行
快速开始
安装
# 安装 Selenium
pip install selenium
# 安装 WebDriver 管理工具(推荐)
pip install webdriver-manager
WebDriver
Selenium 4+ 中使用新的 W3C 标准:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
# 方式 1:使用 webdriver-manager(推荐)
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
# Chrome
driver = webdriver.Chrome(ChromeDriverManager().install())
# Firefox
driver = webdriver.Firefox(GeckoDriverManager().install())
浏览器驱动
手动配置驱动(不推荐,仅作了解):
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
# 手动指定驱动路径
service = Service('/path/to/chromedriver')
driver = webdriver.Chrome(service=service)
# 设置浏览器选项
options = webdriver.ChromeOptions()
options.add_argument('--headless') # 无头模式
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(service=service, options=options)
浏览器选项配置:
from selenium import webdriver
options = webdriver.ChromeOptions()
# 常用选项
options.add_argument('--start-maximized') # 最大化
options.add_argument('--disable-infobars') # 禁用信息栏
options.add_argument('--incognito') # 隐身模式
options.add_argument('--lang=en-US') # 设置语言
# 禁用图片加载(提升速度)
prefs = {'profile.managed_default_content_settings.images': 2}
options.add_experimental_option('prefs', prefs)
# 无头模式(适合 CI/CD)
options.add_argument('--headless')
options.add_argument('--disable-gpu')
# 设置 User-Agent
options.add_argument('user-agent=Custom User Agent')
driver = webdriver.Chrome(options=options)
元素定位
ID定位
from selenium import webdriver
from selenium.webdriver.common.by import By
driver = webdriver.Chrome()
driver.get('https://example.com')
# 通过 ID 定位
element = driver.find_element(By.ID, 'submit-button')
element.click()
# 多个元素(返回列表)
elements = driver.find_elements(By.ID, 'item')
Class定位
# 单个 class
element = driver.find_element(By.CLASS_NAME, 'btn-primary')
# 注意:class 名不能有空格,只匹配单个 class
# 如果要匹配多个 class,使用 CSS 选择器或 XPath
# 多个元素
buttons = driver.find_elements(By.CLASS_NAME, 'button')
for btn in buttons:
print(btn.text)
Tag定位
# 通过标签名
inputs = driver.find_elements(By.TAG_NAME, 'input')
for inp in inputs:
print(inp.get_attribute('type'))
# 获取特定标签
header = driver.find_element(By.TAG_NAME, 'h1')
print(header.text)
Name定位
# 通过 name 属性
search_box = driver.find_element(By.NAME, 'q')
search_box.send_keys('Selenium')
# 表单元素常用
username = driver.find_element(By.NAME, 'username')
password = driver.find_element(By.NAME, 'password')
Link Text定位
# 完整链接文本
link = driver.find_element(By.LINK_TEXT, 'Click Here')
link.click()
# 部分链接文本
link = driver.find_element(By.PARTIAL_LINK_TEXT, 'Click')
link.click()
# 注意:仅对 <a> 标签有效
XPath定位
# 绝对 XPath(不推荐,脆弱)
element = driver.find_element(By.XPATH, '/html/body/div[1]/form[1]/input[1]')
# 相对 XPath(推荐)
element = driver.find_element(By.XPATH, '//input[@id="username"]')
# 通过文本
element = driver.find_element(By.XPATH, '//button[text()="Submit"]')
# 包含文本
element = driver.find_element(By.XPATH, '//a[contains(text(), "Click")]')
# 通过属性
element = driver.find_element(By.XPATH, '//input[@name="email"]')
element = driver.find_element(By.XPATH, '//*[@class="btn-primary"]')
# 属性值包含
element = driver.find_element(By.XPATH, '//div[contains(@class, "container")]')
# 多个属性
element = driver.find_element(By.XPATH, '//input[@type="text" and @name="search"]')
# 父子关系
element = driver.find_element(By.XPATH, '//div[@id="parent"]/input[@class="child"]')
# 轴(Axes)
# following-sibling:后面的兄弟元素
element = driver.find_element(By.XPATH, '//div[@id="current"]/following-sibling::div')
# preceding-sibling:前面的兄弟元素
element = driver.find_element(By.XPATH, '//div[@id="current"]/preceding-sibling::div')
# parent:父元素
element = driver.find_element(By.XPATH, '//a[@id="link"]/parent::div')
# ancestor:祖先元素
element = driver.find_element(By.XPATH, '//a/ancestor::div[@class="container"]')
CSS选择器
CSS 选择器通常比 XPath 更快且更易读:
# ID 选择器
element = driver.find_element(By.CSS_SELECTOR, '#submit-btn')
# Class 选择器
element = driver.find_element(By.CSS_SELECTOR, '.btn-primary')
# 标签选择器
element = driver.find_element(By.CSS_SELECTOR, 'input')
# 属性选择器
element = driver.find_element(By.CSS_SELECTOR, '[name="email"]')
element = driver.find_element(By.CSS_SELECTOR, 'input[type="text"]')
element = driver.find_element(By.CSS_SELECTOR, '[data-value="test"]')
# 属性值包含
element = driver.find_element(By.CSS_SELECTOR, '[class*="btn"]')
element = driver.find_element(By.CSS_SELECTOR, '[href^="https"]') # 以 https 开头
element = driver.find_element(By.CSS_SELECTOR, '[href$=".pdf"]') # 以 .pdf 结尾
# 后代选择器
element = driver.find_element(By.CSS_SELECTOR, 'div.container input')
element = driver.find_element(By.CSS_SELECTOR, '#parent > .child') # 直接子元素
# 组合选择器
element = driver.find_element(By.CSS_SELECTOR, 'input.btn-primary[type="submit"]')
# 伪类
element = driver.find_element(By.CSS_SELECTOR, 'li:first-child')
element = driver.find_element(By.CSS_SELECTOR, 'tr:nth-child(2)')
# 多个选择器(或)
element = driver.find_element(By.CSS_SELECTOR, '#btn1, #btn2, .btn')
定位策略对比:
# ✅ 推荐:ID、Name、CSS Selector
driver.find_element(By.ID, 'user-id')
driver.find_element(By.NAME, 'username')
driver.find_element(By.CSS_SELECTOR, '.submit-btn')
# ⚠️ 谨慎使用:XPath(性能稍差,但功能强大)
driver.find_element(By.XPATH, '//div[@class="container"]/button')
# ❌ 避免:Link Text(仅适用于链接)
# ❌ 避免:绝对 XPath(脆弱)
元素操作
点击
# 普通点击
button = driver.find_element(By.ID, 'submit')
button.click()
# JavaScript 点击(元素被遮挡时使用)
driver.execute_script('arguments[0].click();', button)
# 等待元素可点击
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
button = wait.until(EC.element_to_be_clickable((By.ID, 'submit')))
button.click()
输入文本
from selenium.webdriver.common.keys import Keys
# 清除并输入
text_input = driver.find_element(By.NAME, 'message')
text_input.clear() # 清除现有文本
text_input.send_keys('Hello Selenium')
# 追加文本
text_input.send_keys(' - Additional text')
# 发送特殊按键
text_input.send_keys(Keys.ENTER) # 回车
text_input.send_keys(Keys.TAB) # Tab
text_input.send_keys(Keys.ESCAPE) # Esc
text_input.send_keys(Keys.CONTROL, 'a') # Ctrl+A 全选
text_input.send_keys(Keys.CONTROL, 'c') # Ctrl+C 复制
text_input.send_keys(Keys.CONTROL, 'v') # Ctrl+V 粘贴
# 组合按键
text_input.send_keys(Keys.SHIFT, 'hello') # 输入 HELLO(大写)
清除
# 方式 1:clear() 方法
input_field = driver.find_element(By.NAME, 'email')
input_field.clear()
# 方式 2:全选后删除
input_field.send_keys(Keys.CONTROL, 'a')
input_field.send_keys(Keys.BACKSPACE)
# 方式 3:JavaScript 清除
driver.execute_script('arguments[0].value = ""', input_field)
提交
# 方式 1:点击提交按钮
submit_btn = driver.find_element(By.CSS_SELECTOR, 'button[type="submit"]')
submit_btn.click()
# 方式 2:submit() 方法(仅对 form 内元素有效)
form_input = driver.find_element(By.NAME, 'username')
form_input.send_keys('testuser')
form_input.submit() # 提交表单
# 方式 3:JavaScript 提交
driver.execute_script('document.forms[0].submit()')
获取元素信息
# 获取文本
element = driver.find_element(By.CLASS_NAME, 'content')
text = element.text
print(text)
# 获取属性值
href = element.get_attribute('href')
id_value = element.get_attribute('id')
class_value = element.get_attribute('class')
# 获取 CSS 属性
bg_color = element.value_of_css_property('background-color')
font_size = element.value_of_css_property('font-size')
# 检查元素状态
is_displayed = element.is_displayed() # 是否可见
is_enabled = element.is_enabled() # 是否可用
is_selected = element.is_selected() # 是否选中(单选/复选框)
# 获取标签名
tag_name = element.tag_name
# 获取位置和大小
location = element.location # {'x': 100, 'y': 200}
size = element.size # {'width': 300, 'height': 50}
rect = element.rect # 位置和尺寸
等待机制
强制等待
import time
# 不推荐:固定等待,降低测试效率
time.sleep(5) # 等待 5 秒
# 仅在必要时使用(如等待外部系统)
time.sleep(2)
隐式等待
# 设置全局等待时间
driver.implicitly_wait(10) # 10 秒
# 如果元素未立即找到,每 500ms 检查一次,最多等待 10 秒
driver.find_element(By.ID, 'username')
# 注意:隐式等待对整个 session 生效,只需设置一次
# 缺点:可能掩盖一些时序问题
显式等待
推荐使用显式等待,更加精确和灵活:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 等待元素出现
wait = WebDriverWait(driver, 10)
element = wait.until(
EC.presence_of_element_located((By.ID, 'username'))
)
# 等待元素可见
element = wait.until(
EC.visibility_of_element_located((By.CSS_SELECTOR, '.message'))
)
# 等待元素可点击
element = wait.until(
EC.element_to_be_clickable((By.XPATH, '//button[@type="submit"]'))
)
# 等待文本出现
element = wait.until(
EC.text_to_be_present_in_element((By.ID, 'status'), 'Completed')
)
# 等待标题包含特定文本
wait.until(EC.title_contains('Welcome'))
# 等待 URL 包含特定内容
wait.until(EC.url_contains('/dashboard'))
# 等待元素被选中
wait.until(EC.element_to_be_selected((By.ID, 'checkbox1')))
# 等待所有 frames 可用
wait.until(EC.frame_to_be_available_and_switch_to_it('frame_name'))
# 等待弹窗出现
wait.until(EC.alert_is_present())
自定义等待条件:
# 自定义等待条件
def element_has_css_class(locator, css_class):
def _predicate(driver):
element = driver.find_element(*locator)
return css_class in element.get_attribute('class')
return _predicate
# 使用自定义条件
wait = WebDriverWait(driver, 10)
element = wait.until(
element_has_css_class((By.ID, 'my-element'), 'active')
)
# 使用 lambda(更简洁)
element = wait.until(
lambda d: d.find_element(By.ID, 'my-element').is_displayed()
)
Fluent等待
FluentWait 提供更细粒度的控制:
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException
# FluentWait 配置
wait = WebDriverWait(driver, 30, poll_frequency=2, ignored_exceptions=[NoSuchElementException])
element = wait.until(
lambda d: d.find_element(By.ID, 'dynamic-element')
)
# 参数说明:
# timeout: 30 秒超时
# poll_frequency: 每 2 秒检查一次(默认 500ms)
# ignored_exceptions: 忽略的异常类型
等待最佳实践:
# ✅ 推荐:显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, 'submit')))
element.click()
# ⚠️ 谨慎使用:隐式等待
driver.implicitly_wait(10)
# ❌ 避免:强制等待
time.sleep(5)
浏览器操作
导航
# 打开 URL
driver.get('https://www.example.com')
# 获取当前 URL
current_url = driver.current_url
print(f'Current URL: {current_url}')
# 获取页面标题
title = driver.title
print(f'Page title: {title}')
# 获取页面源代码
page_source = driver.page_source
前进后退
# 后退
driver.back()
# 等同于
driver.execute_script('window.history.back()')
# 前进
driver.forward()
# 等同于
driver.execute_script('window.history.forward()')
# 刷新
driver.refresh()
# 等同于
driver.execute_script('location.reload()')
窗口管理
# 获取当前窗口句柄
current_window = driver.current_window_handle
# 获取所有窗口句柄
all_windows = driver.window_handles
print(f'All windows: {all_windows}')
# 切换到新窗口
for window in all_windows:
if window != current_window:
driver.switch_to.window(window)
break
# 打开新窗口
driver.execute_script('window.open("https://example.com");')
# 关闭当前窗口
driver.close()
# 关闭所有窗口并退出
driver.quit()
# 切换到最新的窗口
driver.switch_to.window(driver.window_handles[-1])
窗口大小和位置:
# 最大化窗口
driver.maximize_window()
# 最小化窗口
driver.minimize_window()
# 全屏
driver.fullscreen_window()
# 设置窗口大小
driver.set_window_size(1920, 1080)
# 设置窗口位置
driver.set_window_position(100, 100)
# 获取窗口大小
size = driver.get_window_size()
print(f'Window size: {size}')
# 获取窗口位置
position = driver.get_window_position()
print(f'Window position: {position}')
切换框架
# 通过索引切换(从 0 开始)
driver.switch_to.frame(0)
# 通过 name 或 id 切换
driver.switch_to.frame('frame_name')
driver.switch_to.frame('frame_id')
# 通过 WebElement 切换
frame_element = driver.find_element(By.XPATH, '//iframe[@name="main"]')
driver.switch_to.frame(frame_element)
# 切换回主文档
driver.switch_to.default_content()
# 嵌套 frame 切换
driver.switch_to.frame('outer_frame')
driver.switch_to.frame('inner_frame')
# 回到 outer_frame
driver.switch_to.parent_frame()
# 回到主文档
driver.switch_to.default_content()
弹窗处理
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 等待弹窗出现
wait = WebDriverWait(driver, 10)
wait.until(EC.alert_is_present())
# 切换到弹窗
alert = driver.switch_to.alert
# 获取弹窗文本
alert_text = alert.text
print(f'Alert text: {alert_text}')
# 确认弹窗(点击 OK)
alert.accept()
# 取消弹窗(点击 Cancel)
alert.dismiss()
# 输入文本(prompt 弹窗)
alert.send_keys('Hello')
alert.accept()
# 处理确认框(confirm)
alert = driver.switch_to.alert
if 'Are you sure?' in alert.text:
alert.accept()
else:
alert.dismiss()
表单操作
输入框
# 文本输入
text_input = driver.find_element(By.NAME, 'username')
text_input.clear()
text_input.send_keys('testuser')
# 只读输入框(使用 JavaScript)
readonly_input = driver.find_element(By.ID, 'readonly-field')
driver.execute_script('arguments[0].value = "new value"', readonly_input)
# 验证输入值
value = text_input.get_attribute('value')
assert value == 'testuser'
下拉框
from selenium.webdriver.support.ui import Select
# 获取下拉框元素
select_element = driver.find_element(By.ID, 'country')
select = Select(select_element)
# 通过可见文本选择
select.select_by_visible_text('China')
# 通过值选择
select.select_by_value('CN')
# 通过索引选择
select.select_by_index(0)
# 获取所有选项
all_options = select.options
for option in all_options:
print(option.text)
# 获取当前选中的选项
selected_option = select.first_selected_option
print(f'Selected: {selected_option.text}')
# 获取所有选中的选项(多选)
selected_options = select.all_selected_options
# 取消选择
select.deselect_by_visible_text('China')
select.deselect_by_value('CN')
select.deselect_by_index(0)
select.deselect_all() # 仅对多选有效
# 检查是否多选
is_multiple = select.is_multiple
复选框
# 查找复选框
checkbox = driver.find_element(By.ID, 'terms')
# 检查是否已选中
if not checkbox.is_selected():
checkbox.click()
# 取消选中
if checkbox.is_selected():
checkbox.click()
# 使用 JavaScript 切换
driver.execute_script('arguments[0].click()', checkbox)
# 多个复选框
checkboxes = driver.find_elements(By.NAME, 'interests')
for checkbox in checkboxes:
if checkbox.get_attribute('value') in ['coding', 'reading']:
if not checkbox.is_selected():
checkbox.click()
单选框
# 查找单选框组
radio_buttons = driver.find_elements(By.NAME, 'gender')
# 选择特定值
for radio in radio_buttons:
if radio.get_attribute('value') == 'male':
if not radio.is_selected():
radio.click()
break
# 直接选择(通过 XPath)
male_radio = driver.find_element(By.XPATH, '//input[@name="gender" and @value="male"]')
male_radio.click()
# 验证选择
selected = driver.find_element(By.XPATH, '//input[@name="gender" and @checked]')
assert selected.get_attribute('value') == 'male'
文件上传
# 方式 1:直接设置 value(最简单)
file_input = driver.find_element(By.CSS_SELECTOR, 'input[type="file"]')
file_input.send_keys('/path/to/file.txt')
# 方式 2:使用 AutoIt(Windows 复杂场景)
from pywinauto import Application
# 点击文件上传按钮
upload_btn = driver.find_element(By.ID, 'upload')
upload_btn.click()
# 操作系统文件选择对话框
app = Application().connect(title='Open')
app.window(title='Open').Edit.set_edit_text('/path/to/file.txt')
app.window(title='Open').Button.click()
# 多文件上传
file_input.send_keys('/path/to/file1.txt\n/path/to/file2.txt')
JavaScript执行
execute_script
# 执行 JavaScript 不返回值
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
# 滚动到元素
element = driver.find_element(By.ID, 'footer')
driver.execute_script('arguments[0].scrollIntoView(true);', element)
# 修改元素样式
driver.execute_script('arguments[0].style.border = "2px solid red"', element)
# 移除元素
driver.execute_script('arguments[0].remove()', element)
# 添加/修改属性
driver.execute_script('arguments[0].setAttribute("data-value", "test")', element)
返回值
# 返回标题
title = driver.execute_script('return document.title;')
# 返回 URL
url = driver.execute_script('return window.location.href;')
# 返回元素属性
value = driver.execute_script('return arguments[0].value;', element)
# 返回元素文本
text = driver.execute_script('return arguments[0].textContent;', element)
# 返回多个值
result = driver.execute_script('''
return {
title: document.title,
url: window.location.href,
scrollY: window.scrollY
};
''')
print(result['title'])
print(result['url'])
print(result['scrollY'])
# 返回元素列表
elements = driver.execute_script('''
return document.querySelectorAll('.item');
''')
print(len(elements)) # 元素数量
常用 JavaScript 操作:
# 滚动操作
# 滚动到页面底部
driver.execute_script('window.scrollTo(0, document.body.scrollHeight);')
# 滚动到页面顶部
driver.execute_script('window.scrollTo(0, 0);')
# 横向滚动
driver.execute_script('window.scrollTo(1000, 0);')
# 获取滚动位置
scroll_y = driver.execute_script('return window.scrollY;')
# 修改窗口大小
driver.execute_script('window.resizeTo(1920, 1080);')
# 获取元素位置
position = driver.execute_script('''
const rect = arguments[0].getBoundingClientRect();
return {x: rect.x, y: rect.y};
''', element)
# 触发事件
driver.execute_script('arguments[0].dispatchEvent(new Event("change"))', element)
截图
元素截图
# 截取特定元素
element = driver.find_element(By.ID, 'main-content')
# 保存到文件
element.screenshot('element_screenshot.png')
# 作为二进制数据获取
screenshot_data = element.screenshot_as_png
with open('element.png', 'wb') as f:
f.write(screenshot_data)
# Base64 编码
screenshot_base64 = element.screenshot_as_base64
页面截图
# 整页截图
driver.save_screenshot('full_page.png')
# 截图数据
screenshot_png = driver.get_screenshot_as_png()
with open('page.png', 'wb') as f:
f.write(screenshot_png)
# Base64 编码(用于嵌入 HTML)
screenshot_base64 = driver.get_screenshot_as_base64()
# 完整页面截图(包括需要滚动的内容)
from PIL import Image
def full_page_screenshot(driver):
# 获取页面总高度
total_height = driver.execute_script('return document.body.scrollHeight')
# 获取窗口大小
window_size = driver.get_window_size()
# 滚动并截图
screenshots = []
for y in range(0, total_height, window_size['height']):
driver.execute_script(f'window.scrollTo(0, {y});')
screenshots.append(driver.get_screenshot_as_png())
# 拼接图片
images = [Image.open(io.BytesIO(s)) for s in screenshots]
total_width = images[0].width
total_height = sum(img.height for img in images)
combined = Image.new('RGB', (total_width, total_height))
y_offset = 0
for img in images:
combined.paste(img, (0, y_offset))
y_offset += img.height
combined.save('full_page.png')
Cookie操作
获取Cookie
# 获取所有 Cookie
all_cookies = driver.get_cookies()
for cookie in all_cookies:
print(f'{cookie["name"]} = {cookie["value"]}')
# 获取特定 Cookie
cookie = driver.get_cookie('session_id')
print(cookie)
# Cookie 属性
print(cookie['name']) # 名称
print(cookie['value']) # 值
print(cookie['domain']) # 域
print(cookie['path']) # 路径
print(cookie['expiry']) # 过期时间
print(cookie['secure']) # 是否安全
print(cookie['httpOnly']) # 是否 HttpOnly
添加Cookie
# 添加单个 Cookie
driver.add_cookie({
'name': 'user',
'value': 'john_doe',
'path': '/',
'domain': '.example.com'
})
# 添加多个 Cookie
cookies = [
{'name': 'session', 'value': 'abc123'},
{'name': 'user', 'value': 'john'},
{'name': 'theme', 'value': 'dark'}
]
for cookie in cookies:
driver.add_cookie(cookie)
# 注意:必须先访问域才能设置 Cookie
driver.get('https://example.com')
driver.add_cookie({'name': 'test', 'value': 'value'})
删除Cookie
# 删除特定 Cookie
driver.delete_cookie('session_id')
# 删除所有 Cookie
driver.delete_all_cookies()
# 验证删除
cookies = driver.get_cookies()
print(f'Remaining cookies: {len(cookies)}')
Cookie 实际应用:
# 保存登录状态
driver.get('https://example.com/login')
# 执行登录操作...
# 保存 Cookie
cookies = driver.get_cookies()
with open('cookies.json', 'w') as f:
json.dump(cookies, f)
# 恢复登录状态
with open('cookies.json', 'r') as f:
cookies = json.load(f)
driver.get('https://example.com')
for cookie in cookies:
driver.add_cookie(cookie)
driver.refresh() # 刷新以应用 Cookie
动作链
鼠标操作
from selenium.webdriver.common.action_chains import ActionChains
# 创建动作链
actions = ActionChains(driver)
# 移动到元素
element = driver.find_element(By.ID, 'menu-item')
actions.move_to_element(element).perform()
# 鼠标悬停
hover_element = driver.find_element(By.CLASS_NAME, 'hover-target')
actions.move_to_element(hover_element).perform()
# 点击并按住
click_and_hold = driver.find_element(By.ID, 'draggable')
actions.click_and_hold(click_and_hold).perform()
# 释放鼠标
actions.release(click_and_hold).perform()
# 双击
double_click_element = driver.find_element(By.ID, 'double-click')
actions.double_click(double_click_element).perform()
# 右键点击
context_menu = driver.find_element(By.ID, 'context-menu')
actions.context_click(context_menu).perform()
键盘操作
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
# 发送按键
actions = ActionChains(driver)
actions.send_keys('Hello World')
actions.send_keys(Keys.ENTER)
actions.perform()
# 组合按键
actions.key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL).perform()
actions.key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
# Shift + 点击
element = driver.find_element(By.ID, 'select-item')
actions.key_down(Keys.SHIFT).click(element).key_up(Keys.SHIFT).perform()
拖拽
from selenium.webdriver.common.action_chains import ActionChains
# 方式 1:drag_and_drop
draggable = driver.find_element(By.ID, 'draggable')
droppable = driver.find_element(By.ID, 'droppable')
actions = ActionChains(driver)
actions.drag_and_drop(draggable, droppable).perform()
# 方式 2:分步操作
actions.click_and_hold(draggable).move_to_element(droppable).release().perform()
# 方式 3:按偏移量移动
actions.click_and_hold(draggable).move_by_offset(100, 200).release().perform()
# 拖拽到指定坐标
actions.click_and_hold(draggable).move_to_location(500, 300).release().perform()
复杂动作链:
from selenium.webdriver.common.action_chains import ActionChains
# 组合多个动作
element = driver.find_element(By.ID, 'target')
actions = ActionChains(driver)
actions.move_to_element(element) # 移动到元素
actions.pause(1) # 暂停 1 秒
actions.click() # 点击
actions.send_keys('test') # 输入文本
actions.pause(0.5)
actions.send_keys(Keys.TAB) # Tab 键
actions.perform()
Page Object模式
设计原则
Page Object 模式是一种设计模式,将页面元素和操作封装成类:
- 分离关注点:页面逻辑与测试逻辑分离
- 可重用性:页面操作可在多个测试中复用
- 易维护:页面变化只需修改 Page Object 类
基础类
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
"""页面基类"""
def __init__(self, driver):
self.driver = driver
self.timeout = 10
def find_element(self, locator):
"""查找单个元素"""
return WebDriverWait(self.driver, self.timeout).until(
EC.visibility_of_element_located(locator)
)
def find_elements(self, locator):
"""查找多个元素"""
return self.driver.find_elements(*locator)
def click(self, locator):
"""点击元素"""
element = self.find_element(locator)
element.click()
def enter_text(self, locator, text):
"""输入文本"""
element = self.find_element(locator)
element.clear()
element.send_keys(text)
def get_text(self, locator):
"""获取元素文本"""
element = self.find_element(locator)
return element.text
def is_visible(self, locator):
"""检查元素是否可见"""
try:
return self.find_element(locator).is_displayed()
except:
return False
def wait_for_title(self, title):
"""等待标题"""
WebDriverWait(self.driver, self.timeout).until(
EC.title_contains(title)
)
def scroll_to_element(self, locator):
"""滚动到元素"""
element = self.find_element(locator)
self.driver.execute_script('arguments[0].scrollIntoView(true);', element)
页面类
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
class LoginPage(BasePage):
"""登录页面对象"""
# 页面 URL
URL = 'https://example.com/login'
# 元素定位器
USERNAME_INPUT = (By.ID, 'username')
PASSWORD_INPUT = (By.ID, 'password')
LOGIN_BUTTON = (By.CSS_SELECTOR, 'button[type="submit"]')
ERROR_MESSAGE = (By.CLASS_NAME, 'error-message')
FORGOT_PASSWORD_LINK = (By.LINK_TEXT, 'Forgot Password?')
def __init__(self, driver):
super().__init__(driver)
def load(self):
"""加载页面"""
self.driver.get(self.URL)
def login(self, username, password):
"""执行登录"""
self.enter_text(self.USERNAME_INPUT, username)
self.enter_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
return HomePage(self.driver) # 返回主页对象
def get_error_message(self):
"""获取错误消息"""
return self.get_text(self.ERROR_MESSAGE)
def click_forgot_password(self):
"""点击忘记密码"""
self.click(self.FORGOT_PASSWORD_LINK)
return ForgotPasswordPage(self.driver)
class HomePage(BasePage):
"""主页面对象"""
URL = 'https://example.com/home'
# 元素定位器
USER_MENU = (By.ID, 'user-menu')
LOGOUT_BUTTON = (By.LINK_TEXT, 'Logout')
WELCOME_MESSAGE = (By.CLASS_NAME, 'welcome')
def __init__(self, driver):
super().__init__(driver)
def is_loaded(self):
"""检查页面是否加载"""
return 'Home' in self.driver.title
def get_welcome_message(self):
"""获取欢迎消息"""
return self.get_text(self.WELCOME_MESSAGE)
def logout(self):
"""退出登录"""
self.click(self.USER_MENU)
self.click(self.LOGOUT_BUTTON)
return LoginPage(self.driver)
class SearchPage(BasePage):
"""搜索页面对象"""
URL = 'https://example.com/search'
# 元素定位器
SEARCH_INPUT = (By.NAME, 'q')
SEARCH_BUTTON = (By.CSS_SELECTOR, 'button.search-btn')
SEARCH_RESULTS = (By.CLASS_NAME, 'result-item')
def __init__(self, driver):
super().__init__(driver)
def search(self, query):
"""执行搜索"""
self.enter_text(self.SEARCH_INPUT, query)
self.click(self.SEARCH_BUTTON)
return self
def get_results_count(self):
"""获取结果数量"""
results = self.find_elements(self.SEARCH_RESULTS)
return len(results)
def get_first_result_text(self):
"""获取第一个结果文本"""
results = self.find_elements(self.SEARCH_RESULTS)
if results:
return results[0].text
return None
使用 Page Object:
import pytest
from selenium import webdriver
from pages.login_page import LoginPage
from pages.home_page import HomePage
class TestLogin:
"""登录测试"""
def setup_method(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(10)
def teardown_method(self):
self.driver.quit()
def test_valid_login(self):
"""测试有效登录"""
# 创建页面对象
login_page = LoginPage(self.driver)
login_page.load()
# 执行登录
home_page = login_page.login('testuser', 'password123')
# 验证结果
assert home_page.is_loaded()
assert 'Welcome' in home_page.get_welcome_message()
def test_invalid_login(self):
"""测试无效登录"""
login_page = LoginPage(self.driver)
login_page.load()
# 使用错误凭据登录
login_page.login('invalid', 'wrongpassword')
# 验证错误消息
error_message = login_page.get_error_message()
assert 'Invalid credentials' in error_message
测试框架集成
pytest + Selenium
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
# Fixtures
@pytest.fixture(scope='function')
def driver():
"""创建 WebDriver 实例"""
driver = webdriver.Chrome(ChromeDriverManager().install())
driver.maximize_window()
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.fixture(scope='session')
def base_url():
"""基础 URL"""
return 'https://example.com'
# 测试类
class TestSearch:
"""搜索功能测试"""
def test_search_by_keyword(self, driver, base_url):
"""关键词搜索测试"""
driver.get(base_url)
# 执行搜索
search_box = driver.find_element(By.NAME, 'q')
search_box.send_keys('Selenium')
search_box.submit()
# 验证结果
assert 'Selenium' in driver.title
results = driver.find_elements(By.CLASS_NAME, 'result')
assert len(results) > 0
@pytest.mark.parametrize('keyword,expected_count', [
('python', 10),
('selenium', 8),
('java', 15),
])
def test_search_various_keywords(self, driver, base_url, keyword, expected_count):
"""参数化搜索测试"""
driver.get(base_url)
search_box = driver.find_element(By.NAME, 'q')
search_box.send_keys(keyword)
search_box.submit()
results = driver.find_elements(By.CLASS_NAME, 'result')
assert len(results) >= expected_count
# 标记测试
@pytest.mark.smoke
@pytest.mark.auth
class TestAuthentication:
"""认证测试"""
def test_login_success(self, driver, base_url):
"""登录成功测试"""
driver.get(f'{base_url}/login')
# 登录逻辑...
assert 'Dashboard' in driver.title
@pytest.mark.skip(reason='Feature not implemented')
def test_social_login(self, driver, base_url):
"""社交登录测试"""
pass
配置文件:
# conftest.py
import pytest
from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
@pytest.fixture(scope='function')
def driver():
"""全局 driver fixture"""
options = webdriver.ChromeOptions()
# CI/CD 环境使用 headless
if os.getenv('CI'):
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(ChromeDriverManager().install(), options=options)
driver.maximize_window()
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.fixture(scope='function')
def login_page(driver):
"""登录页面对象 fixture"""
from pages.login_page import LoginPage
page = LoginPage(driver)
page.load()
return page
@pytest.fixture(scope='function')
def authenticated_driver(driver):
"""已认证的 driver fixture"""
driver.get('https://example.com/login')
# 执行登录
# ...
return driver
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""失败时截图"""
outcome = yield
report = outcome.get_result()
if report.when == 'call' and report.failed:
if 'driver' in item.fixturenames:
driver = item.funcargs['driver']
screenshot_path = f'screenshots/{item.name}.png'
driver.save_screenshot(screenshot_path)
print(f'Screenshot saved: {screenshot_path}')
unittest + Selenium
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
class TestSearch(unittest.TestCase):
"""搜索测试类"""
@classmethod
def setUpClass(cls):
"""整个测试类运行前执行一次"""
cls.driver = webdriver.Chrome(ChromeDriverManager().install())
cls.driver.maximize_window()
@classmethod
def tearDownClass(cls):
"""整个测试类运行后执行一次"""
cls.driver.quit()
def setUp(self):
"""每个测试方法前执行"""
self.driver.get('https://example.com')
def tearDown(self):
"""每个测试方法后执行"""
# 清理操作
pass
def test_search_by_keyword(self):
"""关键词搜索测试"""
search_box = self.driver.find_element(By.NAME, 'q')
search_box.send_keys('Python')
search_box.submit()
self.assertIn('Python', self.driver.title)
results = self.driver.find_elements(By.CLASS_NAME, 'result')
self.assertGreater(len(results), 0)
def test_empty_search(self):
"""空搜索测试"""
search_box = self.driver.find_element(By.NAME, 'q')
search_box.send_keys('')
search_box.submit()
error_message = self.driver.find_element(By.CLASS_NAME, 'error').text
self.assertEqual(error_message, 'Please enter a search term')
if __name__ == '__main__':
unittest.main()
Headless模式
Headless 模式在无图形界面环境中运行浏览器,适合 CI/CD:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# Chrome Headless
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
# 设置窗口大小(某些网站需要)
chrome_options.add_argument('--window-size=1920,1080')
driver = webdriver.Chrome(options=chrome_options)
# Firefox Headless
from selenium.webdriver.firefox.options import Options as FirefoxOptions
firefox_options = FirefoxOptions()
firefox_options.add_argument('--headless')
driver = webdriver.Firefox(options=firefox_options)
配置 Headless 模式:
def create_driver(headless=True):
"""创建 WebDriver"""
options = webdriver.ChromeOptions()
if headless:
options.add_argument('--headless')
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
# 通用选项
options.add_argument('--disable-infobars')
options.add_argument('--disable-extensions')
driver = webdriver.Chrome(options=options)
driver.maximize_window()
return driver
# CI/CD 环境自动启用 headless
import os
is_ci = os.getenv('CI') is not None
driver = create_driver(headless=is_ci)
并行测试
使用 pytest-xdist 并行执行测试:
# 安装
pip install pytest-xdist
# 并行运行(使用所有 CPU)
pytest -n auto
# 使用指定数量的 worker
pytest -n 4
# 按测试文件分配
pytest -n 4 --dist=loadfile
# 按测试类分配
pytest -n 4 --dist=loadscope
Selenium Grid 并行测试:
import pytest
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
@pytest.fixture(params=['chrome', 'firefox'])
def driver(request):
"""多浏览器 fixture"""
if request.param == 'chrome':
driver = webdriver.Chrome()
else:
driver = webdriver.Firefox()
driver.maximize_window()
yield driver
driver.quit()
def test_cross_browser(driver):
"""跨浏览器测试"""
driver.get('https://example.com')
assert 'Example' in driver.title
Selenium Grid 配置:
# 连接到远程 Grid
from selenium import webdriver
capabilities = webdriver.DesiredCapabilities.CHROME
driver = webdriver.Remote(
command_executor='http://localhost:4444/wd/hub',
desired_capabilities=capabilities
)
# Selenium 4 使用 options
from selenium.webdriver.chrome.options import Options
options = Options()
options.set_capability('browserName', 'chrome')
options.set_capability('browserVersion', 'latest')
driver = webdriver.Remote(
command_executor='http://localhost:4444/wd/hub',
options=options
)
最佳实践
1. 使用显式等待
# ✅ 好:显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, 'submit')))
element.click()
# ❌ 不好:强制等待
import time
time.sleep(5)
button = driver.find_element(By.ID, 'submit')
button.click()
2. 使用 Page Object 模式
# ✅ 好:Page Object
login_page = LoginPage(driver)
login_page.login('user', 'pass')
# ❌ 不好:测试逻辑与页面逻辑混合
driver.find_element(By.ID, 'username').send_keys('user')
driver.find_element(By.ID, 'password').send_keys('pass')
driver.find_element(By.ID, 'submit').click()
3. 稳定的元素定位
# ✅ 好:稳定的定位器
driver.find_element(By.ID, 'submit-btn')
driver.find_element(By.CSS_SELECTOR, '[data-testid="submit"]')
# ❌ 不好:脆弱的定位器
driver.find_element(By.XPATH, '/html/body/div[1]/form[1]/button[1]')
4. 失败截图
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == 'call' and report.failed:
if 'driver' in item.fixturenames:
driver = item.funcargs['driver']
screenshot_name = f'{item.name}_failed.png'
driver.save_screenshot(f'screenshots/{screenshot_name}')
5. 测试数据分离
# ✅ 好:测试数据在外部
# test_data.json
{"username": "testuser", "password": "test123"}
# test.py
with open('test_data.json') as f:
data = json.load(f)
login_page.login(data['username'], data['password'])
# ❌ 不好:硬编码
login_page.login('testuser', 'test123')
6. 清理测试环境
# ✅ 好:使用 fixture 清理
@pytest.fixture
def driver():
driver = webdriver.Chrome()
yield driver
# 清理:删除 cookies、关闭浏览器
driver.delete_all_cookies()
driver.quit()
# ❌ 不好:手动清理
def test_something():
driver = webdriver.Chrome()
# 测试...
driver.quit() # 容易忘记或异常时未执行
7. 避免硬编码等待时间
# ✅ 好:动态等待
wait = WebDriverWait(driver, 10)
wait.until(EC.title_contains('Dashboard'))
# ❌ 不好:固定等待
time.sleep(10) # 浪费时间,可能导致不稳定
8. 使用日志记录
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def test_login(driver):
logger.info('Starting login test')
driver.get('https://example.com/login')
logger.info('Page loaded')
username = driver.find_element(By.ID, 'username')
username.send_keys('testuser')
logger.info('Username entered')
# ...
logger.info('Login test completed')
常见问题
元素未找到异常
from selenium.common.exceptions import NoSuchElementException
try:
element = driver.find_element(By.ID, 'nonexistent')
except NoSuchElementException as e:
print(f'Element not found: {e}')
# 处理异常或记录日志
元素不可点击
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import ElementClickInterceptedException
try:
element = driver.find_element(By.ID, 'btn')
element.click()
except ElementClickInterceptedException:
# 元素被遮挡,使用 JavaScript 点击
driver.execute_script('arguments[0].click();', element)
超时处理
from selenium.common.exceptions import TimeoutException
try:
element = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.ID, 'slow-loading'))
)
except TimeoutException:
print('Element did not load within 5 seconds')
# 记录失败或采取其他行动
Session Not Created 异常
# 驱动版本不匹配
# 解决:使用 webdriver-manager 自动管理
from webdriver_manager.chrome import ChromeDriverManager
driver = webdriver.Chrome(ChromeDriverManager().install())
StaleElementReferenceException
from selenium.common.exceptions import StaleElementReferenceException
try:
element.click()
except StaleElementReferenceException:
# 元素已过期,重新查找
element = driver.find_element(By.ID, 'button')
element.click()
通过 Selenium,你可以构建强大的 Web 自动化测试套件,确保 Web 应用的质量和稳定性。