Selenium 高级话题 #
Selenium Grid #
架构概览 #
text
┌─────────────────────────────────────────────────────────────┐
│ Selenium Grid 架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ Hub │ │
│ │ (调度中心) │ │
│ └──────┬──────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Node1 │ │ Node2 │ │ Node3 │ │
│ │ Chrome │ │ Firefox │ │ Safari │ │
│ │ Windows │ │ Linux │ │ macOS │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ 优势: │
│ • 并行执行测试 │
│ • 跨浏览器测试 │
│ • 跨平台测试 │
│ • 资源优化利用 │
│ │
└─────────────────────────────────────────────────────────────┘
Grid 4 配置 #
toml
# config.toml
[server]
host = "0.0.0.0"
port = 4444
[node]
detect-drivers = false
max-sessions = 5
[[node.driver-configuration]]
display-name = "Chrome"
stereotype = '{"browserName": "chrome", "platformName": "linux"}'
max-sessions = 3
[[node.driver-configuration]]
display-name = "Firefox"
stereotype = '{"browserName": "firefox", "platformName": "linux"}'
max-sessions = 2
启动 Grid #
bash
# 启动独立服务器(Hub + Node)
java -jar selenium-server-4.x.jar standalone
# 分别启动 Hub 和 Node
java -jar selenium-server-4.x.jar hub
java -jar selenium-server-4.x.jar node
# 使用 Docker
docker run -d -p 4444:4444 --name selenium-hub selenium/hub
docker run -d --link selenium-hub:hub selenium/node-chrome
docker run -d --link selenium-hub:hub selenium/node-firefox
远程执行 #
python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
# 连接到 Grid
options = Options()
options.set_capability("browserName", "chrome")
options.set_capability("platformName", "linux")
driver = webdriver.Remote(
command_executor="http://localhost:4444/wd/hub",
options=options
)
driver.get("https://example.com")
print(driver.title)
driver.quit()
多浏览器并行测试 #
python
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options as ChromeOptions
from selenium.webdriver.firefox.options import Options as FirefoxOptions
def create_remote_driver(browser, hub_url="http://localhost:4444/wd/hub"):
"""创建远程驱动"""
if browser == "chrome":
options = ChromeOptions()
options.set_capability("browserName", "chrome")
elif browser == "firefox":
options = FirefoxOptions()
options.set_capability("browserName", "firefox")
else:
raise ValueError(f"不支持的浏览器: {browser}")
return webdriver.Remote(command_executor=hub_url, options=options)
@pytest.fixture(params=["chrome", "firefox"])
def driver(request):
driver = create_remote_driver(request.param)
yield driver
driver.quit()
def test_cross_browser(driver):
driver.get("https://example.com")
assert driver.title
性能测试 #
页面加载性能 #
python
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
def measure_page_load(driver, url):
"""测量页面加载时间"""
start = time.time()
driver.get(url)
# 等待页面完全加载
driver.execute_script("return document.readyState") == "complete"
end = time.time()
return end - start
def measure_performance_metrics(driver):
"""获取性能指标"""
navigation_timing = driver.execute_script("""
return JSON.stringify(window.performance.timing);
""")
import json
timing = json.loads(navigation_timing)
metrics = {
"DNS查询": timing["domainLookupEnd"] - timing["domainLookupStart"],
"TCP连接": timing["connectEnd"] - timing["connectStart"],
"请求响应": timing["responseEnd"] - timing["requestStart"],
"DOM解析": timing["domComplete"] - timing["domInteractive"],
"页面加载": timing["loadEventEnd"] - timing["navigationStart"],
}
return metrics
# 使用示例
driver = webdriver.Chrome()
driver.get("https://example.com")
metrics = measure_performance_metrics(driver)
for name, value in metrics.items():
print(f"{name}: {value}ms")
driver.quit()
网络性能分析 #
python
from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
import json
def enable_network_logging():
"""启用网络日志"""
caps = DesiredCapabilities.CHROME.copy()
caps["goog:loggingPrefs"] = {"performance": "ALL"}
return caps
def get_network_logs(driver):
"""获取网络日志"""
logs = driver.get_log("performance")
requests = []
for entry in logs:
message = json.loads(entry["message"])
message = message.get("message", {})
if message.get("method") == "Network.requestWillBeSent":
request = message.get("params", {}).get("request", {})
requests.append({
"url": request.get("url"),
"method": request.get("method"),
"time": entry["timestamp"]
})
return requests
# 使用示例
caps = enable_network_logging()
driver = webdriver.Chrome(desired_capabilities=caps)
driver.get("https://example.com")
requests = get_network_logs(driver)
for req in requests[:10]:
print(f"{req['method']} {req['url']}")
driver.quit()
资源监控 #
python
from selenium import webdriver
import psutil
import time
class ResourceMonitor:
"""资源监控器"""
def __init__(self, driver):
self.driver = driver
self.chrome_pid = None
def start(self):
"""开始监控"""
for proc in psutil.process_iter(['pid', 'name']):
if 'chrome' in proc.info['name'].lower():
self.chrome_pid = proc.info['pid']
break
def get_memory_usage(self):
"""获取内存使用"""
if self.chrome_pid:
proc = psutil.Process(self.chrome_pid)
return proc.memory_info().rss / 1024 / 1024 # MB
return 0
def get_cpu_usage(self):
"""获取 CPU 使用"""
if self.chrome_pid:
proc = psutil.Process(self.chrome_pid)
return proc.cpu_percent()
return 0
# 使用示例
driver = webdriver.Chrome()
monitor = ResourceMonitor(driver)
monitor.start()
driver.get("https://example.com")
print(f"内存使用: {monitor.get_memory_usage():.2f} MB")
print(f"CPU 使用: {monitor.get_cpu_usage():.2f}%")
driver.quit()
视觉测试 #
截图对比 #
python
from selenium import webdriver
from PIL import Image
import os
def compare_screenshots(image1_path, image2_path, diff_path=None, threshold=0.1):
"""对比两张截图"""
img1 = Image.open(image1_path)
img2 = Image.open(image2_path)
if img1.size != img2.size:
return False, "图片尺寸不同"
diff = Image.new('RGB', img1.size)
pixels_diff = 0
total_pixels = img1.size[0] * img1.size[1]
for x in range(img1.size[0]):
for y in range(img1.size[1]):
pixel1 = img1.getpixel((x, y))
pixel2 = img2.getpixel((x, y))
if pixel1 != pixel2:
diff.putpixel((x, y), (255, 0, 0))
pixels_diff += 1
else:
diff.putpixel((x, y), pixel1)
diff_ratio = pixels_diff / total_pixels
if diff_path and pixels_diff > 0:
diff.save(diff_path)
return diff_ratio < threshold, f"差异比例: {diff_ratio:.2%}"
# 使用示例
driver = webdriver.Chrome()
driver.get("https://example.com")
# 保存基准截图
driver.save_screenshot("baseline.png")
# 保存当前截图
driver.save_screenshot("current.png")
# 对比
is_same, message = compare_screenshots("baseline.png", "current.png")
print(message)
driver.quit()
元素截图 #
python
from selenium import webdriver
from selenium.webdriver.common.by import By
from PIL import Image
from io import BytesIO
def capture_element(driver, locator, output_path):
"""截取元素截图"""
element = driver.find_element(*locator)
# 获取元素位置和大小
location = element.location
size = element.size
# 截取整个页面
screenshot = driver.get_screenshot_as_png()
# 裁剪元素区域
img = Image.open(BytesIO(screenshot))
left = location['x']
top = location['y']
right = left + size['width']
bottom = top + size['height']
element_img = img.crop((left, top, right, bottom))
element_img.save(output_path)
# 使用示例
driver = webdriver.Chrome()
driver.get("https://example.com")
capture_element(driver, (By.ID, "logo"), "logo.png")
driver.quit()
移动端测试 #
移动端模拟 #
python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def create_mobile_driver(device_name="iPhone 12"):
"""创建移动端模拟驱动"""
mobile_emulation = {
"deviceName": device_name
}
options = Options()
options.add_experimental_option("mobileEmulation", mobile_emulation)
return webdriver.Chrome(options=options)
# 使用示例
driver = create_mobile_driver("iPhone 12")
driver.get("https://example.com")
print(f"窗口大小: {driver.get_window_size()}")
print(f"User-Agent: {driver.execute_script('return navigator.userAgent')}")
driver.quit()
自定义移动设备 #
python
def create_custom_mobile_driver():
"""创建自定义移动设备模拟"""
mobile_emulation = {
"deviceMetrics": {
"width": 375,
"height": 812,
"pixelRatio": 3.0
},
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15"
}
options = Options()
options.add_experimental_option("mobileEmulation", mobile_emulation)
return webdriver.Chrome(options=options)
触摸操作模拟 #
python
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
def simulate_touch_operations(driver):
"""模拟触摸操作"""
actions = ActionChains(driver)
element = driver.find_element(By.ID, "target")
# 模拟触摸点击
actions.click(element).perform()
# 模拟长按
actions.click_and_hold(element).pause(1).release().perform()
# 模拟滑动
actions.click_and_hold(element).move_by_offset(0, -100).release().perform()
# 模拟双指缩放(需要使用 JavaScript)
driver.execute_script("""
var element = arguments[0];
var rect = element.getBoundingClientRect();
var centerX = rect.left + rect.width / 2;
var centerY = rect.top + rect.height / 2;
var touch1 = new Touch({
identifier: 1,
target: element,
clientX: centerX - 50,
clientY: centerY
});
var touch2 = new Touch({
identifier: 2,
target: element,
clientX: centerX + 50,
clientY: centerY
});
var touchEvent = new TouchEvent('touchstart', {
touches: [touch1, touch2]
});
element.dispatchEvent(touchEvent);
""", element)
CI/CD 集成 #
Jenkins 配置 #
groovy
// Jenkinsfile
pipeline {
agent any
stages {
stage('Setup') {
steps {
sh 'pip install -r requirements.txt'
}
}
stage('Test') {
steps {
sh '''
pytest tests/ \
--html=reports/report.html \
--self-contained-html \
--alluredir=reports/allure-results
'''
}
}
stage('Report') {
steps {
allure includeProperties: false, jdk: '', results: [[path: 'reports/allure-results']]
publishHTML(target: [
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'reports',
reportFiles: 'report.html',
reportName: 'Test Report'
])
}
}
}
post {
always {
cleanWs()
}
}
}
GitHub Actions #
yaml
# .github/workflows/test.yml
name: Selenium Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
selenium:
image: selenium/standalone-chrome
ports:
- 4444:4444
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests
env:
SELENIUM_HUB_URL: http://localhost:4444/wd/hub
run: |
pytest tests/ --alluredir=reports/allure-results
- name: Generate Allure Report
uses: simple-elf/allure-report-action@master
with:
allure_results: reports/allure-results
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: allure-report
path: allure-history
无头浏览器 #
Headless Chrome #
python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def create_headless_driver():
"""创建无头浏览器驱动"""
options = Options()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
options.add_argument('--window-size=1920,1080')
return webdriver.Chrome(options=options)
# 使用示例
driver = create_headless_driver()
driver.get("https://example.com")
print(driver.title)
driver.quit()
性能对比 #
python
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def compare_headless_performance(url, iterations=5):
"""对比有头和无头模式性能"""
# 有头模式
options = Options()
driver = webdriver.Chrome(options=options)
headful_times = []
for _ in range(iterations):
start = time.time()
driver.get(url)
headful_times.append(time.time() - start)
driver.quit()
# 无头模式
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
headless_times = []
for _ in range(iterations):
start = time.time()
driver.get(url)
headless_times.append(time.time() - start)
driver.quit()
print(f"有头模式平均: {sum(headful_times)/len(headful_times):.2f}s")
print(f"无头模式平均: {sum(headless_times)/len(headless_times):.2f}s")
扩展工具 #
Selenium Wire(网络拦截) #
python
# pip install selenium-wire
from seleniumwire import webdriver
def intercept_network():
"""拦截网络请求"""
driver = webdriver.Chrome()
# 拦截请求
def interceptor(request):
if request.url.endswith('.js'):
request.abort()
driver.request_interceptor = interceptor
driver.get("https://example.com")
# 查看请求
for request in driver.requests:
if request.response:
print(f"{request.method} {request.url} -> {request.response.status_code}")
driver.quit()
BrowserMob Proxy #
python
from browsermobproxy import Server
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def use_proxy():
"""使用代理"""
server = Server("/path/to/browsermob-proxy")
server.start()
proxy = server.create_proxy()
options = Options()
options.add_argument(f'--proxy-server={proxy.proxy}')
driver = webdriver.Chrome(options=options)
proxy.new_har("example")
driver.get("https://example.com")
har = proxy.har
for entry in har['log']['entries']:
print(f"{entry['request']['method']} {entry['request']['url']}")
driver.quit()
server.stop()
调试技巧 #
远程调试 #
python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def enable_remote_debugging():
"""启用远程调试"""
options = Options()
options.add_argument('--remote-debugging-port=9222')
driver = webdriver.Chrome(options=options)
return driver
# 访问 chrome://inspect 进行调试
VNC 远程查看 #
python
# 使用 Docker 启动带 VNC 的 Selenium
# docker run -d -p 4444:4444 -p 5900:5900 selenium/standalone-chrome-debug
# 使用 VNC 客户端连接 localhost:5900 查看测试执行
学习路径总结 #
text
┌─────────────────────────────────────────────────────────────┐
│ Selenium 学习路径 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 入门阶段 │
│ ├── Selenium 简介 │
│ ├── 安装与配置 │
│ ├── 基础使用 │
│ └── 元素定位 │
│ │
│ 进阶阶段 │
│ ├── 用户交互 │
│ ├── 等待机制 │
│ ├── 高级处理 │
│ └── Actions 链操作 │
│ │
│ 高级阶段 │
│ ├── Pytest 集成 │
│ ├── 页面对象模式 │
│ ├── 最佳实践 │
│ └── 高级话题 │
│ │
│ 专家阶段 │
│ ├── Selenium Grid │
│ ├── 性能测试 │
│ ├── CI/CD 集成 │
│ └── 测试架构设计 │
│ │
└─────────────────────────────────────────────────────────────┘
恭喜完成! #
你已经完成了 Selenium Web 自动化测试的完整学习路径。现在你已经具备了:
- 扎实的 Selenium 基础知识
- 熟练的元素定位和操作技能
- 完善的测试框架设计能力
- 专业的最佳实践经验
- 高级的测试扩展技能
继续实践和探索,成为一名优秀的自动化测试工程师!
最后更新:2026-03-28