Selenium 最佳实践 #
代码规范 #
项目结构规范 #
text
┌─────────────────────────────────────────────────────────────┐
│ 推荐项目结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ project/ │
│ ├── config/ # 配置文件 │
│ │ ├── settings.py # 配置类 │
│ │ └── config.yaml # YAML 配置 │
│ ├── pages/ # 页面对象 │
│ │ ├── base_page.py # 基础页面 │
│ │ └── *_page.py # 具体页面 │
│ ├── tests/ # 测试用例 │
│ │ ├── conftest.py # pytest 配置 │
│ │ └── test_*.py # 测试文件 │
│ ├── utils/ # 工具类 │
│ │ ├── driver_factory.py # 驱动工厂 │
│ │ └── helpers.py # 辅助函数 │
│ ├── data/ # 测试数据 │
│ ├── reports/ # 测试报告 │
│ ├── screenshots/ # 截图目录 │
│ ├── pytest.ini # pytest 配置 │
│ └── requirements.txt # 依赖文件 │
│ │
└─────────────────────────────────────────────────────────────┘
命名规范 #
python
# ✅ 好的命名
class LoginPage(BasePage):
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
def login(self, username, password):
pass
def test_login_with_valid_credentials():
pass
# ❌ 不好的命名
class lp(BasePage):
u = (By.ID, "username")
def l(self, u, p):
pass
def test1():
pass
代码注释 #
python
class LoginPage(BasePage):
"""登录页面对象类
封装登录页面的所有元素定位和操作方法
Attributes:
USERNAME_INPUT: 用户名输入框定位器
PASSWORD_INPUT: 密码输入框定位器
LOGIN_BUTTON: 登录按钮定位器
"""
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "submit")
def login(self, username: str, password: str) -> None:
"""执行登录操作
Args:
username: 用户名
password: 密码
Returns:
None
"""
self.enter_text(self.USERNAME_INPUT, username)
self.enter_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
元素定位最佳实践 #
定位器优先级 #
python
# 优先级从高到低
# 1. ID - 最快最稳定
USERNAME = (By.ID, "username")
# 2. Name - 表单元素常用
PASSWORD = (By.NAME, "password")
# 3. CSS Selector - 灵活强大
SUBMIT_BUTTON = (By.CSS_SELECTOR, "button[type='submit']")
# 4. XPath - 功能最全,但性能较低
ERROR_MESSAGE = (By.XPATH, "//div[contains(@class, 'error')]")
# 5. Class Name - 可能不唯一
ITEMS = (By.CLASS_NAME, "item")
# 6. Tag Name - 通常用于批量操作
LINKS = (By.TAG_NAME, "a")
# 7. Link Text - 仅用于链接
ABOUT_LINK = (By.LINK_TEXT, "关于我们")
稳定定位器策略 #
python
# ✅ 使用稳定的属性
SUBMIT_BUTTON = (By.CSS_SELECTOR, "[data-testid='submit-button']")
USERNAME_INPUT = (By.CSS_SELECTOR, "[name='username']")
# ❌ 避免使用不稳定的属性
# 动态生成的 ID
USERNAME = (By.ID, "input_12345") # ID 可能变化
# 过于具体的路径
BUTTON = (By.XPATH, "/html/body/div[2]/form/div[3]/button") # 结构变化会失败
# ✅ 使用相对定位
BUTTON = (By.XPATH, "//button[contains(text(), '提交')]")
# ✅ 组合定位提高准确性
LOGIN_BUTTON = (By.CSS_SELECTOR, "form#login button[type='submit']")
定位器管理 #
python
# pages/locators.py
from selenium.webdriver.common.by import By
class LoginPageLocators:
"""登录页面定位器集中管理"""
# 输入框
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
# 按钮
LOGIN_BUTTON = (By.ID, "submit")
FORGOT_PASSWORD_LINK = (By.LINK_TEXT, "忘记密码")
# 消息
ERROR_MESSAGE = (By.CLASS_NAME, "error-message")
SUCCESS_MESSAGE = (By.CLASS_NAME, "success-message")
等待策略最佳实践 #
显式等待优先 #
python
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# ✅ 使用显式等待
def wait_for_element(driver, locator, timeout=10):
return WebDriverWait(driver, timeout).until(
EC.presence_of_element_located(locator)
)
# ❌ 避免使用隐式等待和显式等待混用
# driver.implicitly_wait(10) # 不要同时使用
# ❌ 避免使用固定等待
# time.sleep(5) # 不要使用
合理的等待条件 #
python
# 根据场景选择合适的等待条件
# 等待元素出现
EC.presence_of_element_located(locator)
# 等待元素可见
EC.visibility_of_element_located(locator)
# 等待元素可点击
EC.element_to_be_clickable(locator)
# 等待元素消失
EC.invisibility_of_element_located(locator)
# 等待文本出现
EC.text_to_be_present_in_element(locator, text)
# 等待 URL 变化
EC.url_contains("dashboard")
等待工具类 #
python
# utils/wait_helper.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
class WaitHelper:
def __init__(self, driver, default_timeout=10):
self.driver = driver
self.default_timeout = default_timeout
def wait_for_clickable(self, locator, timeout=None):
"""等待元素可点击"""
timeout = timeout or self.default_timeout
try:
return WebDriverWait(self.driver, timeout).until(
EC.element_to_be_clickable(locator)
)
except TimeoutException:
self._handle_timeout(locator, "可点击")
def wait_for_visible(self, locator, timeout=None):
"""等待元素可见"""
timeout = timeout or self.default_timeout
return WebDriverWait(self.driver, timeout).until(
EC.visibility_of_element_located(locator)
)
def wait_for_invisible(self, locator, timeout=None):
"""等待元素不可见"""
timeout = timeout or self.default_timeout
return WebDriverWait(self.driver, timeout).until(
EC.invisibility_of_element_located(locator)
)
def _handle_timeout(self, locator, condition):
"""处理超时"""
raise TimeoutException(
f"等待元素 {locator} {condition} 超时"
)
错误处理最佳实践 #
异常捕获 #
python
from selenium.common.exceptions import (
NoSuchElementException,
TimeoutException,
StaleElementReferenceException,
ElementNotInteractableException
)
def safe_click(driver, locator, timeout=10):
"""安全的点击操作"""
try:
element = WebDriverWait(driver, timeout).until(
EC.element_to_be_clickable(locator)
)
element.click()
return True
except TimeoutException:
print(f"元素 {locator} 等待超时")
return False
except ElementNotInteractableException:
print(f"元素 {locator} 不可交互")
# 尝试 JavaScript 点击
driver.execute_script("arguments[0].click();", element)
return True
except Exception as e:
print(f"点击失败: {e}")
return False
重试机制 #
python
import time
from functools import wraps
def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
"""重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts - 1:
raise
print(f"第 {attempt + 1} 次尝试失败: {e}")
time.sleep(delay)
return None
return wrapper
return decorator
# 使用示例
@retry(max_attempts=3, delay=2, exceptions=(TimeoutException,))
def click_with_retry(driver, locator):
element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable(locator)
)
element.click()
失败截图 #
python
# tests/conftest.py
import pytest
import os
from datetime import datetime
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""测试失败时自动截图"""
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get('driver')
if driver:
screenshot_dir = "screenshots"
os.makedirs(screenshot_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{item.name}_{timestamp}.png"
filepath = os.path.join(screenshot_dir, filename)
driver.save_screenshot(filepath)
print(f"\n截图已保存: {filepath}")
性能优化 #
减少不必要的操作 #
python
# ❌ 不好的做法
def test_example(driver):
driver.get("https://example.com")
# 每次都重新定位
for i in range(10):
driver.find_element(By.ID, "item").click()
# ✅ 好的做法
def test_example(driver):
driver.get("https://example.com")
# 缓存元素引用
item = driver.find_element(By.ID, "item")
for i in range(10):
item.click()
批量操作 #
python
# ❌ 逐个操作
for item in items:
item.click()
# ✅ 使用 JavaScript 批量操作
driver.execute_script("""
var items = arguments[0];
for (var i = 0; i < items.length; i++) {
items[i].click();
}
""", items)
禁用不必要的资源 #
python
from selenium.webdriver.chrome.options import Options
options = Options()
# 禁用图片加载
prefs = {"profile.managed_default_content_settings.images": 2}
options.add_experimental_option("prefs", prefs)
# 禁用 CSS(如果不需要)
# prefs = {"profile.managed_default_content_settings.stylesheets": 2}
driver = webdriver.Chrome(options=options)
并行执行 #
python
# pytest.ini
[pytest]
addopts = -n auto
# 或命令行
# pytest -n 4 tests/
测试数据管理 #
数据分离 #
python
# data/test_data.py
class LoginData:
VALID_USER = {
"username": "test_user",
"password": "test_password"
}
INVALID_USERS = [
{"username": "", "password": "pass", "expected_error": "用户名不能为空"},
{"username": "user", "password": "", "expected_error": "密码不能为空"},
{"username": "wrong", "password": "wrong", "expected_error": "用户名或密码错误"},
]
# tests/test_login.py
from data.test_data import LoginData
def test_login_valid(driver, login_page):
login_page.login(**LoginData.VALID_USER)
assert login_page.is_login_success()
@pytest.mark.parametrize("data", LoginData.INVALID_USERS)
def test_login_invalid(driver, login_page, data):
login_page.login(data["username"], data["password"])
assert data["expected_error"] in login_page.get_error_message()
环境配置 #
python
# config/settings.py
import os
from dataclasses import dataclass
@dataclass
class Config:
# 环境
ENV = os.getenv("ENV", "test")
# 基础配置
BASE_URL = {
"dev": "https://dev.example.com",
"test": "https://test.example.com",
"prod": "https://example.com"
}[ENV]
# 超时配置
IMPLICIT_WAIT = 10
PAGE_LOAD_TIMEOUT = 30
# 测试账号
TEST_USERNAME = os.getenv("TEST_USERNAME", "test_user")
TEST_PASSWORD = os.getenv("TEST_PASSWORD", "test_password")
config = Config()
测试稳定性 #
避免硬编码等待 #
python
# ❌ 硬编码等待
time.sleep(5)
element.click()
# ✅ 智能等待
WebDriverWait(driver, 10).until(
EC.element_to_be_clickable(locator)
).click()
处理动态元素 #
python
# 使用部分匹配
element = driver.find_element(By.CSS_SELECTOR, "[id^='username_']")
# 使用 contains
element = driver.find_element(By.XPATH, "//*[contains(@id, 'username')]")
# 等待元素稳定
def wait_for_stable(driver, locator, timeout=10):
"""等待元素位置稳定"""
element = driver.find_element(*locator)
prev_location = element.location
WebDriverWait(driver, timeout).until(
lambda d: d.find_element(*locator).location == prev_location
)
return element
处理 Stale Element #
python
from selenium.common.exceptions import StaleElementReferenceException
def safe_find_element(driver, locator, max_retries=3):
"""安全查找元素,处理 StaleElementException"""
for attempt in range(max_retries):
try:
element = driver.find_element(*locator)
element.is_displayed() # 触发 StaleElementException
return element
except StaleElementReferenceException:
if attempt == max_retries - 1:
raise
time.sleep(0.5)
日志和报告 #
日志配置 #
python
# utils/logger.py
import logging
import os
from datetime import datetime
def setup_logger(name, log_file=None, level=logging.INFO):
"""配置日志"""
logger = logging.getLogger(name)
logger.setLevel(level)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
# 控制台输出
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
# 文件输出
if log_file:
os.makedirs(os.path.dirname(log_file), exist_ok=True)
file_handler = logging.FileHandler(log_file)
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
logger = setup_logger("selenium", "logs/test.log")
# 使用
logger.info("开始执行测试")
logger.error("测试失败")
测试报告 #
python
# 使用 Allure 报告
import allure
from selenium import webdriver
@allure.feature("登录功能")
@allure.story("用户登录")
def test_login(driver, login_page):
"""测试登录功能"""
with allure.step("打开登录页面"):
login_page.open()
allure.attach(
driver.get_screenshot_as_png(),
name="登录页面",
attachment_type=allure.attachment_type.PNG
)
with allure.step("输入用户名和密码"):
login_page.login("user", "password")
with allure.step("验证登录成功"):
assert login_page.is_login_success()
持续集成 #
CI 配置示例 #
yaml
# .github/workflows/test.yml
name: Selenium Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
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
run: |
pytest tests/ --html=reports/report.html --self-contained-html
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: test-report
path: reports/
检查清单 #
测试用例检查 #
text
□ 是否使用了稳定的定位器?
□ 是否使用了显式等待?
□ 是否有适当的错误处理?
□ 测试是否独立(不依赖其他测试)?
□ 测试数据是否分离?
□ 是否有清晰的断言?
□ 失败时是否有截图?
□ 是否有适当的日志?
代码质量检查 #
text
□ 是否遵循命名规范?
□ 是否有适当的注释?
□ 是否使用了页面对象模式?
□ 是否避免了代码重复?
□ 是否处理了异常情况?
□ 是否有合理的超时设置?
□ 是否清理了测试数据?
□ 是否关闭了浏览器?
下一步 #
掌握了最佳实践后,接下来学习 高级话题 了解更多进阶内容!
最后更新:2026-03-28