Selenium IDE导出的Python脚本太乱?手把手教你优化与封装,打造可复用的测试模块

当Selenium IDE自动生成的Python脚本第一次出现在你眼前时,那种扑面而来的混乱感可能让你措手不及——冗余的导入、硬编码的URL、重复的操作逻辑,还有那个看起来像是从Java穿越过来的类结构。这就像收到一份未经整理的原材料清单,而非可以直接下厨的食谱。本文将带你从工程化角度,将这些"原材料"转化为可维护、可复用的测试代码库。

1. 解剖Selenium IDE生成的Python脚本

典型的Selenium IDE导出脚本包含几个明显的"代码异味":

# 原始生成的冗余代码示例
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities  # 从未使用的导入
self.vars = {}  # 未被利用的变量字典
WebDriverWait(driver, 10).until(expected_conditions.presence_of_element_located((By.ID, "foo")))  # 不必要的等待

安全删除指南 (保留核心功能的最小集合):

  1. 移除所有未实际使用的import语句
  2. 删除空的 self.vars 字典及其相关操作
  3. 简化过度设计的类结构(除非确实需要setup/teardown)
  4. 合并重复的元素定位操作

注意:删除前务必确认代码功能不受影响,建议配合git进行版本控制

2. 参数化配置:从硬编码到可配置

将以下硬编码值提取为配置参数:

  • 浏览器驱动路径
  • 测试目标URL
  • 窗口尺寸
  • 隐式等待时间

推荐使用Python的 configparser 管理配置文件:

; config.ini
[DRIVER]
path = /usr/local/bin/msedgedriver
implicit_wait = 10

[TEST_SITE]
url = https://www.baidu.com
window_size = 1088,816

对应的读取代码:

from configparser import ConfigParser

config = ConfigParser()
config.read('config.ini')

driver_path = config.get('DRIVER', 'path')
target_url = config.get('TEST_SITE', 'url')
width, height = map(int, config.get('TEST_SITE', 'window_size').split(','))

3. 使用Pytest框架重构测试用例

原始脚本的测试组织方式存在几个问题:

  • 缺乏断言验证
  • 没有清晰的测试隔离
  • 难以生成测试报告

改造后的Pytest实现:

import pytest
from selenium import webdriver

@pytest.fixture
def browser():
    driver = webdriver.Edge()
    driver.implicitly_wait(10)
    yield driver
    driver.quit()

def test_search_functionality(browser):
    browser.get("https://www.baidu.com")
    search_box = browser.find_element(By.ID, "kw")
    search_box.send_keys("pytest selenium")
    search_box.send_keys(Keys.RETURN)
    
    # 验证搜索结果
    assert "pytest" in browser.title
    results = browser.find_elements(By.CSS_SELECTOR, ".result h3")
    assert len(results) > 0

关键改进点:

  • 使用fixture管理浏览器生命周期
  • 添加了结果验证断言
  • 支持并行测试执行
  • 可生成HTML测试报告

4. 页面操作封装与复用

将常见操作抽象为独立函数/类:

class BaiduPage:
    def __init__(self, driver):
        self.driver = driver
        self.search_input = (By.ID, "kw")
        self.search_button = (By.ID, "su")

    def search(self, keyword):
        self.driver.find_element(*self.search_input).send_keys(keyword)
        self.driver.find_element(*self.search_button).click()
        return SearchResultsPage(self.driver)

class SearchResultsPage:
    def __init__(self, driver):
        self.driver = driver
        self.results = (By.CSS_SELECTOR, ".result h3")

    def get_result_titles(self):
        return [el.text for el in self.driver.find_elements(*self.results)]

使用示例:

def test_search_flow(browser):
    page = BaiduPage(browser)
    results_page = page.search("自动化测试")
    titles = results_page.get_result_titles()
    assert any("selenium" in t.lower() for t in titles)

5. 高级封装技巧

等待策略优化

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)
    )

# 使用示例
element = wait_for_element(driver, (By.ID, "dynamic-content"))

日志记录增强

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('test.log'),
        logging.StreamHandler()
    ]
)

def log_action(func):
    def wrapper(*args, **kwargs):
        logging.info(f"Executing: {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_action
def click_element(driver, locator):
    driver.find_element(*locator).click()

跨浏览器支持

def create_driver(browser_name="edge"):
    if browser_name.lower() == "chrome":
        return webdriver.Chrome()
    elif browser_name.lower() == "firefox":
        return webdriver.Firefox()
    else:
        return webdriver.Edge()

6. 持续集成准备

将优化后的测试模块集成到CI流程中:

.github/workflows/test.yml 示例:

name: Selenium Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: windows-latest
    
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install selenium pytest
    - 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

7. 常见问题解决方案

元素定位失败处理

def safe_click(driver, locator, max_retries=3):
    for attempt in range(max_retries):
        try:
            driver.find_element(*locator).click()
            return True
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(1 * (attempt + 1))
    return False

测试数据管理

import csv

def get_test_data(filename):
    with open(filename, newline='') as csvfile:
        return list(csv.DictReader(csvfile))

# 数据驱动测试示例
@pytest.mark.parametrize("data", get_test_data("search_terms.csv"))
def test_multiple_searches(browser, data):
    page = BaiduPage(browser)
    results_page = page.search(data["term"])
    assert data["expected"] in results_page.get_result_titles()[0]

截图与错误收集

def take_screenshot(driver, name):
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    path = f"screenshots/{name}_{timestamp}.png"
    driver.save_screenshot(path)
    return path

@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:
        driver = item.funcargs.get("browser")
        if driver:
            screenshot = take_screenshot(driver, item.name)
            report.extra = [pytest_html.extras.image(screenshot)]

更多推荐