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