在Web开发和测试领域,模拟用户操作是一项基础但至关重要的技能。想象一下,当你需要测试一个电商网站的购物流程时,手动点击每个按钮、填写每个表单不仅耗时费力,而且难以保证每次操作的一致性。这就是Selenium这类自动化工具大显身手的地方。
我曾在一次电商大促前的压力测试中,用Selenium编写了200个并发用户的购物车操作脚本。通过自动化模拟,我们在3小时内完成了原本需要10人团队工作3天的测试任务,还发现了3个手工测试难以触发的边缘case。
Selenium支持所有主流浏览器,但不同浏览器需要对应的驱动:
提示:建议使用Chrome+chromedriver组合,这是目前最稳定且功能最全的方案。记得将驱动文件放在系统PATH路径或项目目录下。
以Python为例,基础安装只需:
bash复制pip install selenium
但实际项目中我建议安装完整测试套件:
bash复制pip install selenium pytest pytest-html allure-pytest
Selenium提供了多种元素定位方式,按优先级排序:
python复制driver.find_element(By.CSS_SELECTOR, "#login-btn")
python复制driver.find_element(By.XPATH, "//button[@id='login-btn']")
实战经验:优先使用CSS Selector,遇到动态元素再考虑XPath。我整理过一份CSS选择器速查表,覆盖了95%的定位场景。
python复制element.click() # 点击
element.send_keys("text") # 输入
element.clear() # 清空
悬停操作(需ActionChains):
python复制from selenium.webdriver.common.action_chains import ActionChains
ActionChains(driver).move_to_element(element).perform()
拖放操作:
python复制ActionChains(driver).drag_and_drop(source, target).perform()
传统input标签:
python复制driver.find_element(By.CSS_SELECTOR, "input[type='file']").send_keys(file_path)
复杂控件(需AutoIT或PyWinAuto):
python复制import pywinauto
app = pywinauto.Desktop()
app["打开"].child_window(title="文件名:").set_text(file_path)
app["打开"]["打开(&O)"].click()
| 等待类型 | 实现方式 | 适用场景 | 优缺点 |
|---|---|---|---|
| 强制等待 | time.sleep() | 调试阶段 | 简单但效率低下 |
| 隐式等待 | driver.implicitly_wait() | 全局设置 | 可能产生不必要等待 |
| 显式等待 | WebDriverWait | 生产环境 | 精准但代码量多 |
python复制# 全局隐式等待(兜底)
driver.implicitly_wait(5)
# 关键操作显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.ID, "submit-btn"))
)
python复制def login(username, password):
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys(username)
driver.find_element(By.ID, "password").send_keys(password)
# 处理验证码
if "captcha" in driver.page_source:
captcha = solve_captcha(driver.find_element(By.CSS_SELECTOR, ".captcha-img"))
driver.find_element(By.ID, "captcha").send_keys(captcha)
driver.find_element(By.ID, "login-btn").click()
# 验证登录成功
WebDriverWait(driver, 10).until(
EC.url_contains("dashboard")
)
python复制import threading
def add_to_cart(user_credentials):
driver = webdriver.Chrome()
login(driver, **user_credentials)
for item in items:
driver.find_element(By.XPATH, f"//div[contains(text(),'{item}')]/../button").click()
WebDriverWait(driver, 3).until(
EC.text_to_be_present_in_element((By.CLASS_NAME, "cart-count"), str(i+1))
)
driver.quit()
# 启动10个并发用户
threads = []
for i in range(10):
t = threading.Thread(target=add_to_cart, args=(users[i],))
threads.append(t)
t.start()
for t in threads:
t.join()
当遇到跨域限制时,可以:
启动浏览器时添加参数:
python复制options.add_argument("--disable-web-security")
options.add_argument("--allow-running-insecure-content")
使用代理中间件:
python复制from selenium.webdriver.common.proxy import Proxy, ProxyType
proxy = Proxy({
'proxyType': ProxyType.MANUAL,
'httpProxy': 'corporate-proxy:8080'
})
python复制from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless") # 无头模式
options.add_argument("--disable-gpu") # 禁用GPU加速
options.add_argument("--window-size=1920,1080") # 固定窗口尺寸
options.add_argument("--blink-settings=imagesEnabled=false") # 禁用图片加载
driver = webdriver.Chrome(options=options)
通过DevTools Protocol拦截非必要请求:
python复制from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
caps = DesiredCapabilities.CHROME
caps['goog:loggingPrefs'] = {'performance': 'ALL'}
driver = webdriver.Chrome(desired_capabilities=caps)
def intercept_request(request):
if request['request']['url'].endswith(('.jpg', '.png', '.gif')):
return {'errorCode': -32000}
return None
driver.execute_cdp_cmd('Network.setRequestInterception', {
'patterns': [{'urlPattern': '*', 'resourceType': 'Image'}]
})
将页面封装为类,提高代码可维护性:
python复制class LoginPage:
def __init__(self, driver):
self.driver = driver
self.username = (By.ID, "username")
self.password = (By.ID, "password")
self.submit = (By.ID, "login-btn")
def login(self, username, password):
self.driver.find_element(*self.username).send_keys(username)
self.driver.find_element(*self.password).send_keys(password)
self.driver.find_element(*self.submit).click()
return DashboardPage(self.driver)
典型框架结构:
code复制project/
├── pages/
│ ├── login_page.py
│ └── product_page.py
├── tests/
│ ├── test_login.py
│ └── test_checkout.py
├── utils/
│ ├── config.py
│ └── logger.py
└── conftest.py
在pytest中的使用示例:
python复制import pytest
from pages.login_page import LoginPage
@pytest.fixture
def login_page(driver):
return LoginPage(driver)
def test_successful_login(login_page):
dashboard = login_page.login("valid_user", "valid_pass")
assert dashboard.is_displayed()
python复制from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument("--headless")
options.add_argument("--disable-gpu")
options.add_argument("--no-sandbox") # Linux系统必加
driver = webdriver.Chrome(options=options)
python复制# 全屏截图
driver.save_screenshot("fullpage.png")
# 元素截图
element = driver.find_element(By.TAG_NAME, "body")
element.screenshot("element.png")
# 生成PDF(需要Chrome 63+)
driver.execute_cdp_cmd("Page.printToPDF", {
"landscape": False,
"displayHeaderFooter": False,
"printBackground": True,
"paperWidth": 8.27,
"paperHeight": 11.69,
"marginTop": 0,
"marginBottom": 0,
"marginLeft": 0,
"marginRight": 0
})
with open("page.pdf", "wb") as f:
f.write(base64.b64decode(pdf['data']))
python复制from selenium.webdriver.chrome.options import Options
mobile_emulation = {
"deviceMetrics": {"width": 375, "height": 812, "pixelRatio": 3.0},
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2 like Mac OS X) ..."
}
options = Options()
options.add_experimental_option("mobileEmulation", mobile_emulation)
driver = webdriver.Chrome(options=options)
使用TouchAction类(Appium扩展):
python复制from appium.webdriver.common.touch_action import TouchAction
actions = TouchAction(driver)
actions.tap_and_hold(x=100, y=200).move_to(x=300, y=200).release().perform()
python复制from PIL import Image
import pytesseract
def solve_captcha(element):
element.screenshot("captcha.png")
img = Image.open("captcha.png")
text = pytesseract.image_to_string(img)
return text.strip()
重要提示:处理验证码时要遵守网站服务条款,仅限合法授权测试使用。
python复制# 获取页面加载时间
load_time = driver.execute_script(
"return performance.timing.loadEventEnd - performance.timing.navigationStart;"
)
# 获取资源加载详情
resources = driver.execute_script("return window.performance.getEntriesByType('resource');")
python复制# 获取内存使用情况
js_heap = driver.execute_script("return window.performance.memory.usedJSHeapSize;")
# 定期检查内存增长
def check_memory_leak():
baseline = None
for _ in range(10):
current = driver.execute_script("return window.performance.memory.usedJSHeapSize;")
if baseline and current > baseline * 1.5:
print("Possible memory leak detected!")
baseline = current if not baseline else baseline
time.sleep(1)
启动Hub:
bash复制java -jar selenium-server-standalone.jar -role hub
注册Node:
bash复制java -jar selenium-server-standalone.jar -role node -hub http://hub-ip:4444/grid/register
Python客户端连接:
python复制from selenium.webdriver import Remote
driver = Remote(
command_executor='http://hub-ip:4444/wd/hub',
desired_capabilities={'browserName': 'chrome'}
)
dockerfile复制FROM selenium/standalone-chrome
COPY test_script.py /home/seluser/
CMD ["python", "/home/seluser/test_script.py"]
启动命令:
bash复制docker build -t selenium-test .
docker run --network host selenium-test
python复制payloads = [
"<script>alert(1)</script>",
"<img src=x onerror=alert(1)>",
"'\"><svg/onload=alert(1)>"
]
for payload in payloads:
driver.find_element(By.ID, "search").send_keys(payload)
driver.find_element(By.ID, "submit").click()
try:
alert = driver.switch_to.alert
print(f"Vulnerable to: {payload}")
alert.accept()
except:
pass
python复制# 获取CSRF Token
token = driver.find_element(By.NAME, "csrf_token").get_attribute("value")
# 在请求头中添加
driver.execute_script("""
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/transfer', true);
xhr.setRequestHeader('X-CSRF-Token', arguments[0]);
xhr.send(JSON.stringify({amount: 100, to: 'attacker'}));
""", token)
python复制import logging
from selenium.webdriver.remote.remote_connection import LOGGER
LOGGER.setLevel(logging.WARNING)
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('automation.log'),
logging.StreamHandler()
]
)
def click_element(element, description=""):
try:
element.click()
logging.info(f"Clicked {description}")
except Exception as e:
logging.error(f"Failed to click {description}: {str(e)}")
raise
python复制import allure
import pytest
@allure.feature("登录功能")
class TestLogin:
@allure.story("成功登录")
def test_success_login(self, login_page):
with allure.step("输入正确凭证"):
login_page.enter_credentials("valid", "valid")
with allure.step("点击登录按钮"):
dashboard = login_page.submit()
with allure.step("验证跳转"):
assert dashboard.is_displayed()
python复制# 修改WebDriver属性
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
})
# 修改浏览器特征
options.add_argument("--disable-blink-features=AutomationControlled")
python复制import random
import time
def human_type(element, text):
for char in text:
element.send_keys(char)
time.sleep(random.uniform(0.1, 0.3))
# 随机误删并重新输入
if random.random() > 0.7:
for _ in range(random.randint(1, 3)):
element.send_keys(Keys.BACK_SPACE)
time.sleep(random.uniform(0.2, 0.5))
human_type(element, text[-random.randint(1,3):])
python复制from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
desired_cap = {
'os': 'Windows',
'os_version': '10',
'browser': 'Chrome',
'browser_version': 'latest',
'name': 'BStack-[Python] Sample Test'
}
driver = webdriver.Remote(
command_executor='https://USERNAME:ACCESS_KEY@hub.browserstack.com/wd/hub',
desired_capabilities=desired_cap)
python复制# 创建测试包
zip -r tests.zip tests/ pages/ utils/
# 使用AWS CLI上传
aws devicefarm create-upload --project-arn YOUR_PROJECT_ARN \
--name tests.zip --type APPIUM_PYTHON_TEST_PACKAGE
python复制import cv2
import numpy as np
def compare_images(img1_path, img2_path):
img1 = cv2.imread(img1_path)
img2 = cv2.imread(img2_path)
diff = cv2.absdiff(img1, img2)
gray = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
_, threshold = cv2.threshold(gray, 25, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
return len(contours) == 0
python复制from applitools.selenium import Eyes
eyes = Eyes()
eyes.api_key = 'YOUR_API_KEY'
try:
eyes.open(driver, "App Name", "Test Name")
eyes.check_window("Main Page")
eyes.close()
finally:
eyes.abort_if_not_closed()
python复制# 模拟地理位置
driver.execute_cdp_cmd("Emulation.setGeolocationOverride", {
"latitude": 37.7749,
"longitude": -122.4194,
"accuracy": 100
})
# 模拟陀螺仪
driver.execute_cdp_cmd("Emulation.setDeviceMetricsOverride", {
"deviceScaleFactor": 3,
"mobile": True,
"fitWindow": False
})
python复制from selenium.webdriver.common.proxy import Proxy, ProxyType
proxy = Proxy({
'proxyType': ProxyType.MANUAL,
'httpProxy': 'localhost:8080',
'sslProxy': 'localhost:8080'
})
caps = webdriver.DesiredCapabilities.CHROME
proxy.add_to_capabilities(caps)
driver = webdriver.Chrome(desired_capabilities=caps)
groovy复制pipeline {
agent any
stages {
stage('Checkout') {
steps {
git 'https://github.com/your/repo.git'
}
}
stage('Test') {
steps {
sh 'python -m pytest tests/ --html=report.html'
}
}
stage('Report') {
steps {
publishHTML target: [
allowMissing: false,
alwaysLinkToLastBuild: false,
keepAll: true,
reportDir: '.',
reportFiles: 'report.html',
reportName: 'HTML Report'
]
}
}
}
}
yaml复制name: Selenium Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
selenium:
image: selenium/standalone-chrome
ports:
- 4444:4444
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
python -m pytest tests/ --html=report.html
- name: Upload report
uses: actions/upload-artifact@v2
with:
name: test-report
path: report.html