1. 项目概述与核心价值最近在重构一个老旧的积载系统一个用于物流、仓储或运输领域的货物装载规划系统的自动化测试脚本正好把整个SeleniumPython的UI自动化测试框架又从头到尾捋了一遍。很多人觉得UI自动化测试就是“录屏回放”或者写几个find_element、click就完事了但真正要在项目中落地尤其是面对像积载系统这样包含复杂交互、动态数据、状态流转的业务系统时你会发现从环境搭建到脚本稳定运行中间有无数个坑等着你。这篇文章我就以这个积载系统为实例拆解一个完整的、可复用的UI自动化测试代码结构并分享那些官方文档里不会写的实战经验和避坑技巧。无论你是刚接触Selenium的新手还是想优化现有框架的老手相信都能从中找到可以直接“抄作业”的干货。这个实例的核心不仅仅是展示如何用Selenium操作浏览器更重要的是构建一个健壮、可维护、易扩展的测试框架。我们会涵盖从环境准备、元素定位策略、等待机制、数据驱动、测试报告生成到持续集成集成的完整链路。你会发现一个稳定的自动化测试其代码量可能只占30%剩下的70%都是围绕“稳定性”和“可维护性”所做的架构设计。2. 环境搭建与核心依赖解析工欲善其事必先利其器。一个稳定、一致的环境是自动化测试的基石。很多新手卡在第一步就是因为环境没配好各种奇怪的报错接踵而至。2.1 Python环境与包管理首先我强烈建议使用Python 3.8及以上的稳定版本。太老的版本如Python 2.7已停止维护而太新的版本如Python 3.12初期可能遇到一些第三方库的兼容性问题。安装时务必勾选“Add Python to PATH”这是很多后续问题的根源。包管理上使用pip即可。但为了隔离项目环境避免包冲突我习惯为每个自动化项目创建独立的虚拟环境。使用venv模块非常简单# 在项目根目录下执行 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate激活后你的命令行提示符前会出现(venv)标识之后所有pip install操作都只影响这个虚拟环境。接下来安装核心依赖。除了selenium我们还需要一些辅助库来让框架更强大。pip install selenium pip install webdriver-manager # 自动管理浏览器驱动强烈推荐 pip install pytest # 测试运行框架比unittest更强大灵活 pip install pytest-html # 生成HTML测试报告 pip install pytest-xdist # 支持分布式测试加速执行 pip install openpyxl # 读写Excel文件用于数据驱动 pip install allure-pytest # 生成Allure美观测试报告可选但推荐注意webdriver-manager是一个神器。它解决了手动下载、匹配和配置ChromeDriver、GeckoDriver等浏览器驱动的繁琐和版本冲突问题。只需在代码中指定它会自动下载匹配当前浏览器版本的正确驱动。2.2 浏览器与驱动管理我们以Chrome为例。确保你安装了稳定版的Chrome浏览器。不建议使用Beta或Dev版本它们可能引入不稳定的变更。在代码中我们这样使用webdriver-manager来启动浏览器from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options def create_driver(): chrome_options Options() # 以下是关键配置直接影响测试稳定性和速度 chrome_options.add_argument(--disable-gpu) # 禁用GPU加速在某些虚拟环境下更稳定 chrome_options.add_argument(--no-sandbox) # 在Linux或Docker环境中常需添加 chrome_options.add_argument(--disable-dev-shm-usage) # 解决共享内存问题 chrome_options.add_argument(--window-size1920,1080) # 设定初始窗口大小 # 可选无头模式不打开浏览器GUI适合CI/CD环境 # chrome_options.add_argument(--headlessnew) # Chrome 109 推荐使用new # 使用webdriver-manager自动管理驱动 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionschrome_options) # 隐式等待为整个会话设置一个全局的查找元素超时时间 driver.implicitly_wait(10) # 单位秒 return driver实操心得--disable-dev-shm-usage这个参数在Linux服务器或Docker容器中运行无头浏览器时至关重要。默认的/dev/shm分区可能太小导致Chrome崩溃。加上这个参数会让Chrome使用/tmp目录避免此问题。3. 测试框架设计与核心模块拆解一个散乱的脚本集是无法维护的。我们需要一个清晰的分层架构。我采用的典型结构如下project_root/ ├── conftest.py # Pytest全局配置、Fixture定义 ├── pytest.ini # Pytest配置文件 ├── requirements.txt # 项目依赖 ├── common/ # 公共模块 │ ├── __init__.py │ ├── base_page.py # 页面基类封装通用操作 │ ├── config.py # 配置文件读取 │ └── logger.py # 日志模块 ├── page_objects/ # 页面对象模型PO │ ├── __init__.py │ ├── login_page.py # 登录页面 │ ├── main_dashboard.py # 主面板 │ └── stowage_plan_page.py # 积载计划页面核心 ├── test_cases/ # 测试用例 │ ├── __init__.py │ ├── test_login.py │ └── test_stowage_plan.py ├── test_data/ # 测试数据 │ ├── users.json │ └── stowage_cases.xlsx ├── reports/ # 测试报告输出目录 │ └── (由pytest-html或allure自动生成) └── drivers/ # (可选) 手动放置驱动的地方但更推荐用webdriver-manager3.1 页面对象模型Page Object Model, POM的精髓POM是UI自动化的最佳实践核心思想是将页面元素定位和页面操作行为封装成类测试用例只关心业务逻辑不关心底层元素如何定位。这极大提升了代码的可维护性当页面UI改动时你只需要修改对应的Page类而不需要翻遍所有测试脚本。以积载系统的登录页面为例# page_objects/login_page.py from selenium.webdriver.common.by import By from common.base_page import BasePage # 假设有一个封装了通用操作的基类 class LoginPage(BasePage): # 1. 元素定位器Locators集中管理所有元素定位方式 USERNAME_INPUT (By.ID, username) # 优先使用ID最稳定 PASSWORD_INPUT (By.NAME, password) # 其次Name LOGIN_BUTTON (By.CSS_SELECTOR, button.btn-primary) # CSS选择器灵活高效 ERROR_MSG_SPAN (By.CLASS_NAME, alert-error) # 2. 页面操作Actions封装对页面的操作行为 def enter_username(self, username): 输入用户名 self.clear_and_send_keys(self.USERNAME_INPUT, username) def enter_password(self, password): 输入密码 self.clear_and_send_keys(self.PASSWORD_INPUT, password) def click_login(self): 点击登录按钮 self.click(self.LOGIN_BUTTON) def get_error_message(self): 获取错误提示信息 return self.get_text(self.ERROR_MSG_SPAN) # 3. 业务场景组合可选将常用操作流封装成高级方法 def login(self, username, password): 完整的登录流程 self.enter_username(username) self.enter_password(password) self.click_login()为什么选择这些定位方式ID唯一且稳定渲染即确定是首选。但现代前端框架如React, Vue可能生成动态ID需注意。Name常用于表单元素也比较稳定。CSS Selector功能强大速度快支持复杂关系定位如div.content input:first-child。比XPath在大多数浏览器中性能更优。XPath万不得已时使用。它功能最强大但性能相对较差且容易因DOM结构微小变动而失效。仅在元素没有ID、Name、Class且CSS无法精确定位时使用尽量用相对路径如//button[text()提交]而非绝对路径。3.2 等待机制稳定性的生命线UI自动化测试失败十有八九是因为“等待”没做好。Selenium提供了几种等待方式隐式等待Implicit Waitdriver.implicitly_wait(10)。这是一个全局设置告诉WebDriver在查找任何元素时如果元素没有立即出现最多等待10秒。它只对find_element和find_elements方法生效。缺点它不关心元素是否处于“可交互状态”如可点击、可见。通常作为一道基础保险。显式等待Explicit Wait这是最推荐、最核心的等待策略。它允许你为某个特定的条件设置等待条件满足则立即继续超时则抛出异常。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待元素可见并可点击 wait WebDriverWait(driver, 10) # 最长等10秒 element wait.until(EC.element_to_be_clickable((By.ID, submit-btn))) element.click() # 等待元素包含特定文本 wait.until(EC.text_to_be_present_in_element((By.CLASS_NAME, status), 加载完成))常用Expected ConditionsECpresence_of_element_located: 元素存在于DOM树不一定可见。visibility_of_element_located: 元素可见宽高大于0。element_to_be_clickable: 元素可见且可点击。这是点击操作前的最佳等待条件。invisibility_of_element_located: 元素不可见或从DOM中移除常用于等待加载动画消失。强制等待time.sleepimport time; time.sleep(5)。这是最不推荐的方式因为它无条件固定等待浪费执行时间且无法自适应网络或机器性能。仅在调试脚本或处理极特殊、无任何状态可判断的场景时临时使用。在BasePage中的最佳实践将显式等待封装到基类的每个基础操作中。# common/base_page.py class BasePage: def __init__(self, driver): self.driver driver self.wait WebDriverWait(driver, 10) # 定义显式等待对象 def click(self, locator): 点击元素封装了等待 element self.wait.until(EC.element_to_be_clickable(locator)) element.click() def send_keys(self, locator, text): 输入文本封装了等待和清空 element self.wait.until(EC.visibility_of_element_located(locator)) element.clear() element.send_keys(text) def get_text(self, locator): 获取元素文本 element self.wait.until(EC.visibility_of_element_located(locator)) return element.text这样在Page Object中调用self.click(self.LOGIN_BUTTON)时就已经内置了“等待可点击”的逻辑脚本稳定性大幅提升。4. 积载系统核心测试场景实现现在我们进入正题看看如何测试积载系统的一个核心功能创建并计算一个积载计划。4.1 场景分析与步骤拆解假设我们的积载计划页面包含以下步骤从导航菜单进入“积载计划”模块。点击“新建计划”按钮。在表单中输入计划名称、选择船舶、输入航次。添加货物选择货类、输入重量、体积。点击“智能计算”按钮系统进行配载优化。验证计算结果检查总重量、重心、稳性等指标是否在预期范围内。保存计划。4.2 Page Object 实现StowagePlanPage# page_objects/stowage_plan_page.py from selenium.webdriver.common.by import By from selenium.webdriver.support.select import Select from common.base_page import BasePage import time class StowagePlanPage(BasePage): # 定位器 NAV_STOWAGE_LINK (By.LINK_TEXT, 积载计划) CREATE_NEW_BTN (By.ID, create-new-plan) PLAN_NAME_INPUT (By.ID, planName) SHIP_SELECT (By.ID, shipSelect) VOYAGE_INPUT (By.ID, voyage) # 货物行是动态添加的定位第一行的模板 CARGO_TYPE_SELECT (By.CSS_SELECTOR, select.cargo-type:first-of-type) CARGO_WEIGHT_INPUT (By.CSS_SELECTOR, input.cargo-weight:first-of-type) CARGO_VOLUME_INPUT (By.CSS_SELECTOR, input.cargo-volume:first-of-type) ADD_CARGO_BTN (By.ID, add-cargo-row) CALCULATE_BTN (By.ID, calculate-btn) SAVE_PLAN_BTN (By.ID, save-plan) # 结果区域 TOTAL_WEIGHT_SPAN (By.ID, totalWeight) CENTER_OF_GRAVITY_SPAN (By.ID, centerOfGravity) STABILITY_INDICATOR (By.ID, stabilityStatus) def navigate_to_stowage(self): 导航到积载计划页面 self.click(self.NAV_STOWAGE_LINK) # 可以增加一个等待确保页面加载完成例如等待“新建计划”按钮出现 self.wait.until(EC.visibility_of_element_located(self.CREATE_NEW_BTN)) def create_new_plan(self): 点击新建计划 self.click(self.CREATE_NEW_BTN) def fill_basic_info(self, plan_name, ship_name, voyage): 填写计划基本信息 self.send_keys(self.PLAN_NAME_INPUT, plan_name) # 处理下拉框 ship_select_element self.wait.until(EC.presence_of_element_located(self.SHIP_SELECT)) select Select(ship_select_element) select.select_by_visible_text(ship_name) # 根据文本选择 self.send_keys(self.VOYAGE_INPUT, voyage) def add_cargo_item(self, cargo_type, weight, volume): 添加一条货物信息 # 选择货类 cargo_type_select self.wait.until(EC.presence_of_element_located(self.CARGO_TYPE_SELECT)) Select(cargo_type_select).select_by_value(cargo_type) # 根据value选择 # 输入重量和体积 self.send_keys(self.CARGO_WEIGHT_INPUT, str(weight)) self.send_keys(self.CARGO_VOLUME_INPUT, str(volume)) # 点击“添加”按钮增加一行 self.click(self.ADD_CARGO_BTN) # 添加后等待新的一行渲染完成可以简单等待一小会儿或者等待某个元素出现 time.sleep(0.5) # 这里简化处理理想情况是等待新增行的某个元素出现 def perform_calculation(self): 点击智能计算按钮 self.click(self.CALCULATE_BTN) # 关键等待计算完成。通常系统会有加载状态这里假设计算完成后结果区域的文本会更新 self.wait.until(EC.text_to_be_present_in_element(self.TOTAL_WEIGHT_SPAN, 0)) # 等待总重量不再是初始值‘0’或‘--’实际应根据业务逻辑调整等待条件 def get_calculation_results(self): 获取计算结果 total_weight self.get_text(self.TOTAL_WEIGHT_SPAN) cog self.get_text(self.CENTER_OF_GRAVITY_SPAN) stability self.get_text(self.STABILITY_INDICATOR) return { total_weight: float(total_weight.replace(t, )), # 去除单位 center_of_gravity: cog, stability: stability } def save_plan(self): 保存计划 self.click(self.SAVE_PLAN_BTN) # 等待保存成功提示例如一个Toast消息 # self.wait.until(EC.visibility_of_element_located((By.CLASS_NAME, toast-success)))4.3 测试用例编写使用Pytest# test_cases/test_stowage_plan.py import pytest from page_objects.login_page import LoginPage from page_objects.stowage_plan_page import StowagePlanPage class TestStowagePlan: 积载计划功能测试 pytest.fixture(scopefunction) def login(self, driver): 每个测试用例前登录function级别的fixture login_page LoginPage(driver) login_page.navigate_to_login_page(http://your-system-url/login) # 假设基类有导航方法 login_page.login(valid_user, valid_password) yield # 如果需要可以在这里添加登出逻辑 def test_create_and_calculate_stowage_plan(self, driver, login): 测试创建积载计划并计算 # 1. 初始化页面对象 stowage_page StowagePlanPage(driver) # 2. 导航到积载计划页面 stowage_page.navigate_to_stowage() # 3. 创建新计划 stowage_page.create_new_plan() # 4. 填写基本信息 stowage_page.fill_basic_info(自动化测试计划-001, 东方之星号, VY2024001) # 5. 添加货物这里添加两种货物 stowage_page.add_cargo_item(container, 500, 1200) stowage_page.add_cargo_item(bulk_grain, 3000, 8500) # 6. 执行计算 stowage_page.perform_calculation() # 7. 验证计算结果 results stowage_page.get_calculation_results() # 断言总重量应在合理范围内3500 ± 50 expected_min_weight 3450 expected_max_weight 3550 assert expected_min_weight results[total_weight] expected_max_weight, \ f总重量{results[total_weight]}不在预期范围[{expected_min_weight}, {expected_max_weight}]内 # 断言稳性状态应为“良好” assert results[stability] 良好, f稳性状态异常: {results[stability]} # 8. 保存计划可选根据业务决定是否在测试中执行保存 # stowage_page.save_plan() # 可以添加更多测试用例如测试边界值、异常数据等 def test_calculate_with_empty_cargo(self, driver, login): 测试不添加货物直接计算应报错或结果为0 stowage_page StowagePlanPage(driver) stowage_page.navigate_to_stowage() stowage_page.create_new_plan() stowage_page.fill_basic_info(空货物测试, 东方之星号, VY2024002) # 不添加货物直接计算 stowage_page.perform_calculation() results stowage_page.get_calculation_results() assert results[total_weight] 0, 空货物计算总重量应为05. 数据驱动与参数化测试硬编码的测试数据不利于维护和扩展。Pytest的pytest.mark.parametrize装饰器可以轻松实现数据驱动测试。5.1 使用Excel管理测试数据首先准备一个Excel文件test_data/stowage_cases.xlsx用例编号计划名称船舶航次货类1重量1体积1货类2重量2体积2预期总重量下限预期总重量上限预期稳性TC-001计划-标准东方之星VY001container5001200bulk_grain3000850034503550良好TC-002计划-超重巨轮号VY002steel80002000---79508050临界TC-003计划-轻货快艇VY003cotton2005000---180220优秀然后编写一个数据读取工具# common/data_reader.py import openpyxl import json import os def read_excel_to_dict(file_path, sheet_nameSheet1): 读取Excel文件返回字典列表 wb openpyxl.load_workbook(file_path, data_onlyTrue) ws wb[sheet_name] data [] headers [cell.value for cell in next(ws.iter_rows(min_row1, max_row1))] for row in ws.iter_rows(min_row2, values_onlyTrue): row_dict dict(zip(headers, row)) # 过滤掉全为None的行 if any(row): data.append(row_dict) return data5.2 参数化测试用例# test_cases/test_stowage_plan_data_driven.py import pytest from page_objects.login_page import LoginPage from page_objects.stowage_plan_page import StowagePlanPage from common.data_reader import read_excel_to_dict # 从Excel读取测试数据 TEST_DATA_FILE os.path.join(os.path.dirname(__file__), ../test_data/stowage_cases.xlsx) test_cases read_excel_to_dict(TEST_DATA_FILE) class TestStowagePlanDataDriven: pytest.fixture(scopeclass) def login_setup(self, driver): 整个测试类只登录一次class级别fixture login_page LoginPage(driver) login_page.navigate_to_login_page(http://your-system-url/login) login_page.login(valid_user, valid_password) yield # 登出清理 pytest.mark.parametrize(case_data, test_cases, ids[case[用例编号] for case in test_cases]) def test_stowage_plan_with_data(self, driver, login_setup, case_data): 使用外部数据驱动的积载计划测试 stowage_page StowagePlanPage(driver) stowage_page.navigate_to_stowage() stowage_page.create_new_plan() # 使用测试数据填充表单 stowage_page.fill_basic_info( case_data[计划名称], case_data[船舶], case_data[航次] ) # 动态添加货物这里简化假设最多两行货物 cargo_list [] if case_data[货类1]: cargo_list.append((case_data[货类1], case_data[重量1], case_data[体积1])) if case_data.get(货类2): # 使用get避免KeyError cargo_list.append((case_data[货类2], case_data[重量2], case_data[体积2])) for cargo_type, weight, volume in cargo_list: stowage_page.add_cargo_item(cargo_type, weight, volume) stowage_page.perform_calculation() results stowage_page.get_calculation_results() # 断言 assert case_data[预期总重量下限] results[total_weight] case_data[预期总重量上限], \ f总重量{results[total_weight]}不符合预期 assert results[stability] case_data[预期稳性], \ f稳性状态{results[stability]}不符合预期{case_data[预期稳性]}这样我们只需要维护Excel表格就能轻松添加、修改测试用例实现测试逻辑与数据的分离。6. 测试报告与日志记录测试执行完了清晰的结果输出至关重要。Pytest本身支持多种报告格式。6.1 生成HTML报告使用pytest-html插件可以生成直观的HTML报告。 首先在pytest.ini中配置# pytest.ini [pytest] addopts -v --htmlreports/report.html --self-contained-html testpaths test_cases python_files test_*.py python_classes Test* python_functions test_*--self-contained-html参数会将CSS和JS内联到HTML文件中生成单个文件便于分享。执行测试后打开reports/report.html即可查看包含通过率、失败详情、执行时间等信息的报告。6.2 集成Allure报告更美观强大Allure报告提供了更丰富的展示包括步骤详情、附件截图、分类、趋势图等。安装Allure命令行工具需单独安装可从官网下载。运行测试时添加参数pytest --alluredir./reports/allure-results生成并打开报告allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report6.3 日志记录良好的日志能帮助快速定位问题。我们可以配置一个简单的日志模块。# common/logger.py import logging import os from datetime import datetime def setup_logger(nameui_auto_test, log_levellogging.INFO): 配置并返回一个logger实例 # 创建logger logger logging.getLogger(name) logger.setLevel(log_level) # 避免重复添加handler if not logger.handlers: # 创建控制台handler ch logging.StreamHandler() ch.setLevel(log_level) # 创建文件handler按日期分割日志 log_dir logs os.makedirs(log_dir, exist_okTrue) log_file os.path.join(log_dir, fui_test_{datetime.now().strftime(%Y%m%d)}.log) fh logging.FileHandler(log_file, encodingutf-8) fh.setLevel(logging.DEBUG) # 文件日志记录更详细 # 定义格式 formatter logging.Formatter( %(asctime)s - %(name)s - %(levelname)s - %(message)s ) ch.setFormatter(formatter) fh.setFormatter(formatter) # 添加handler到logger logger.addHandler(ch) logger.addHandler(fh) return logger # 在conftest.py或BasePage中全局使用 logger setup_logger()在BasePage的操作中嵌入日志def click(self, locator): 点击元素封装了等待和日志 logger.info(f尝试点击元素: {locator}) try: element self.wait.until(EC.element_to_be_clickable(locator)) element.click() logger.info(f成功点击元素: {locator}) except Exception as e: logger.error(f点击元素失败: {locator}, 错误信息: {e}) # 这里可以附加截图 self._take_screenshot(click_failed) raise7. 常见问题排查与稳定性提升技巧即使框架设计得再好在实际运行中也会遇到各种“坑”。下面是我在积载系统自动化测试中总结的常见问题及解决方案。7.1 元素定位失败NoSuchElementException这是最常见的问题。原因1页面未加载完成/元素未出现。解决使用显式等待WebDriverWaitEC而不是time.sleep或仅靠隐式等待。确保等待的条件是准确的如元素可点击、可见。原因2元素在iframe或shadow DOM内。解决使用driver.switch_to.frame(frame_element)切换到iframe内再进行定位。对于Shadow DOM需要使用JavaScript来穿透定位。# 切换到iframe iframe driver.find_element(By.TAG_NAME, iframe) driver.switch_to.frame(iframe) # 在iframe内操作... driver.switch_to.default_content() # 操作完切回主文档原因3元素是动态生成的定位器不稳定。解决避免使用绝对XPath或依赖固定索引的CSS。尝试使用更稳定的属性如># 不好的定位依赖固定结构 (By.XPATH, /html/body/div[3]/div[2]/div/div[2]/button[1]) # 更好的定位使用文本或属性 (By.XPATH, //button[contains(text(), 智能计算)]) (By.CSS_SELECTOR, button[data-rolecalculate])原因4页面有多个相同特征的元素。解决使用find_elements获取列表然后通过索引或过滤找到目标元素。确保你的定位器能唯一标识目标。7.2 元素交互失败ElementNotInteractableException元素找到了但点击或输入不生效。原因1元素被遮挡如弹窗、遮罩层。解决等待遮挡物消失或者使用JavaScript直接执行点击driver.execute_script(arguments[0].click();, element)。注意JS点击可能不会触发所有原生事件。原因2元素不在视口内。解决使用driver.execute_script(arguments[0].scrollIntoView(true);, element)将元素滚动到可视区域然后再操作。原因3元素状态不可交互如disabled。解决在操作前增加等待条件确保元素处于element_to_be_clickable状态。检查业务逻辑是否前置条件未满足导致按钮禁用。7.3 测试执行速度慢原因1过度使用time.sleep。解决全部替换为显式等待。显式等待在条件满足时会立刻继续最大程度减少等待时间。原因2网络或应用响应慢。解决适当增加显式等待的超时时间如从10秒加到30秒。考虑在测试环境中优化应用性能或使用更稳定的网络。原因3不必要的浏览器启动/关闭。解决使用Pytest的scopesession或scopeclass级别的fixture来复用浏览器实例而不是每个测试用例都重启浏览器。注意测试之间的状态隔离清理Cookies、LocalStorage。7.4 测试在CI/CD环境中不稳定Flaky Tests原因环境差异、资源竞争、 timing issue。解决策略增加重试机制使用pytest-rerunfailures插件对失败的测试自动重试几次。pip install pytest-rerunfailures运行pytest --reruns 3 --reruns-delay 2失败后重试3次每次间隔2秒使用更稳定的定位器与开发约定为关键测试元素添加># 在conftest.py中 import pytest pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: # 获取driver fixture假设它叫driver driver_fixture item.funcargs.get(driver) if driver_fixture: screenshot_path f./screenshots/failure_{item.name}_{datetime.now().strftime(%H%M%S)}.png driver_fixture.save_screenshot(screenshot_path) # 将截图路径附加到报告 report.extra [pytest_html.extras.image(screenshot_path, Failure Screenshot)]7.5 处理弹窗和浏览器通知积载系统可能会有各种浏览器弹窗alert, confirm, prompt或通知。# 等待并处理JavaScript Alert from selenium.webdriver.common.alert import Alert alert Alert(driver) print(alert.text) # 获取弹窗文本 alert.accept() # 点击“确定” # alert.dismiss() # 点击“取消” # alert.send_keys(input text) # 向prompt输入文本 # 处理浏览器通知需要在Options中提前设置 chrome_options.add_experimental_option(prefs, { profile.default_content_setting_values.notifications: 2 # 1-允许2-阻止 })8. 持续集成CI集成示例将自动化测试集成到CI/CD流水线中才能实现其最大价值。这里以GitHub Actions为例展示一个简单的配置。# .github/workflows/ui-test.yml name: UI Automation Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest # 使用Linux runner steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install system dependencies (for Chrome) run: | sudo apt-get update sudo apt-get install -y wget unzip # 安装Chrome wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - echo deb [archamd64] http://dl.google.com/linux/chrome/deb/ stable main | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run UI Tests with Headless Chrome env: # 可以在这里设置测试环境URL等变量 BASE_URL: ${{ secrets.TEST_BASE_URL }} run: | # 使用pytest运行测试生成多种报告 pytest -v \ --htmlreports/report.html \ --self-contained-html \ --alluredirreports/allure-results \ --reruns 2 \ --reruns-delay 1 - name: Upload HTML Report uses: actions/upload-artifactv3 if: always() # 即使测试失败也上传报告 with: name: ui-test-html-report path: reports/report.html - name: Upload Allure Results uses: actions/upload-artifactv3 if: always() with: name: allure-results path: reports/allure-results这个工作流会在代码推送或拉取请求时自动触发在Ubuntu环境中安装依赖、运行测试并将测试报告作为制品保存供后续查看。构建一个健壮的Selenium UI自动化测试框架远不止是写几个find_element和click。它涉及到环境治理、架构设计、等待策略、数据管理、报告生成和CI/CD集成等一系列工程化实践。对于像积载系统这样复杂的业务应用一个考虑周全的框架是测试脚本能够长期稳定运行、高效维护的根本保障。在实际项目中还需要根据团队规范、技术栈和业务特点进行灵活调整例如引入Page Factory模式、BDDBehave框架、或者容器化测试环境等。希望这个基于真实项目的完整实例能为你提供一条清晰的实践路径。
Selenium+Python UI自动化测试框架实战:从环境搭建到CI/CD集成
发布时间:2026/7/1 2:25:30
1. 项目概述与核心价值最近在重构一个老旧的积载系统一个用于物流、仓储或运输领域的货物装载规划系统的自动化测试脚本正好把整个SeleniumPython的UI自动化测试框架又从头到尾捋了一遍。很多人觉得UI自动化测试就是“录屏回放”或者写几个find_element、click就完事了但真正要在项目中落地尤其是面对像积载系统这样包含复杂交互、动态数据、状态流转的业务系统时你会发现从环境搭建到脚本稳定运行中间有无数个坑等着你。这篇文章我就以这个积载系统为实例拆解一个完整的、可复用的UI自动化测试代码结构并分享那些官方文档里不会写的实战经验和避坑技巧。无论你是刚接触Selenium的新手还是想优化现有框架的老手相信都能从中找到可以直接“抄作业”的干货。这个实例的核心不仅仅是展示如何用Selenium操作浏览器更重要的是构建一个健壮、可维护、易扩展的测试框架。我们会涵盖从环境准备、元素定位策略、等待机制、数据驱动、测试报告生成到持续集成集成的完整链路。你会发现一个稳定的自动化测试其代码量可能只占30%剩下的70%都是围绕“稳定性”和“可维护性”所做的架构设计。2. 环境搭建与核心依赖解析工欲善其事必先利其器。一个稳定、一致的环境是自动化测试的基石。很多新手卡在第一步就是因为环境没配好各种奇怪的报错接踵而至。2.1 Python环境与包管理首先我强烈建议使用Python 3.8及以上的稳定版本。太老的版本如Python 2.7已停止维护而太新的版本如Python 3.12初期可能遇到一些第三方库的兼容性问题。安装时务必勾选“Add Python to PATH”这是很多后续问题的根源。包管理上使用pip即可。但为了隔离项目环境避免包冲突我习惯为每个自动化项目创建独立的虚拟环境。使用venv模块非常简单# 在项目根目录下执行 python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate激活后你的命令行提示符前会出现(venv)标识之后所有pip install操作都只影响这个虚拟环境。接下来安装核心依赖。除了selenium我们还需要一些辅助库来让框架更强大。pip install selenium pip install webdriver-manager # 自动管理浏览器驱动强烈推荐 pip install pytest # 测试运行框架比unittest更强大灵活 pip install pytest-html # 生成HTML测试报告 pip install pytest-xdist # 支持分布式测试加速执行 pip install openpyxl # 读写Excel文件用于数据驱动 pip install allure-pytest # 生成Allure美观测试报告可选但推荐注意webdriver-manager是一个神器。它解决了手动下载、匹配和配置ChromeDriver、GeckoDriver等浏览器驱动的繁琐和版本冲突问题。只需在代码中指定它会自动下载匹配当前浏览器版本的正确驱动。2.2 浏览器与驱动管理我们以Chrome为例。确保你安装了稳定版的Chrome浏览器。不建议使用Beta或Dev版本它们可能引入不稳定的变更。在代码中我们这样使用webdriver-manager来启动浏览器from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options def create_driver(): chrome_options Options() # 以下是关键配置直接影响测试稳定性和速度 chrome_options.add_argument(--disable-gpu) # 禁用GPU加速在某些虚拟环境下更稳定 chrome_options.add_argument(--no-sandbox) # 在Linux或Docker环境中常需添加 chrome_options.add_argument(--disable-dev-shm-usage) # 解决共享内存问题 chrome_options.add_argument(--window-size1920,1080) # 设定初始窗口大小 # 可选无头模式不打开浏览器GUI适合CI/CD环境 # chrome_options.add_argument(--headlessnew) # Chrome 109 推荐使用new # 使用webdriver-manager自动管理驱动 service Service(ChromeDriverManager().install()) driver webdriver.Chrome(serviceservice, optionschrome_options) # 隐式等待为整个会话设置一个全局的查找元素超时时间 driver.implicitly_wait(10) # 单位秒 return driver实操心得--disable-dev-shm-usage这个参数在Linux服务器或Docker容器中运行无头浏览器时至关重要。默认的/dev/shm分区可能太小导致Chrome崩溃。加上这个参数会让Chrome使用/tmp目录避免此问题。3. 测试框架设计与核心模块拆解一个散乱的脚本集是无法维护的。我们需要一个清晰的分层架构。我采用的典型结构如下project_root/ ├── conftest.py # Pytest全局配置、Fixture定义 ├── pytest.ini # Pytest配置文件 ├── requirements.txt # 项目依赖 ├── common/ # 公共模块 │ ├── __init__.py │ ├── base_page.py # 页面基类封装通用操作 │ ├── config.py # 配置文件读取 │ └── logger.py # 日志模块 ├── page_objects/ # 页面对象模型PO │ ├── __init__.py │ ├── login_page.py # 登录页面 │ ├── main_dashboard.py # 主面板 │ └── stowage_plan_page.py # 积载计划页面核心 ├── test_cases/ # 测试用例 │ ├── __init__.py │ ├── test_login.py │ └── test_stowage_plan.py ├── test_data/ # 测试数据 │ ├── users.json │ └── stowage_cases.xlsx ├── reports/ # 测试报告输出目录 │ └── (由pytest-html或allure自动生成) └── drivers/ # (可选) 手动放置驱动的地方但更推荐用webdriver-manager3.1 页面对象模型Page Object Model, POM的精髓POM是UI自动化的最佳实践核心思想是将页面元素定位和页面操作行为封装成类测试用例只关心业务逻辑不关心底层元素如何定位。这极大提升了代码的可维护性当页面UI改动时你只需要修改对应的Page类而不需要翻遍所有测试脚本。以积载系统的登录页面为例# page_objects/login_page.py from selenium.webdriver.common.by import By from common.base_page import BasePage # 假设有一个封装了通用操作的基类 class LoginPage(BasePage): # 1. 元素定位器Locators集中管理所有元素定位方式 USERNAME_INPUT (By.ID, username) # 优先使用ID最稳定 PASSWORD_INPUT (By.NAME, password) # 其次Name LOGIN_BUTTON (By.CSS_SELECTOR, button.btn-primary) # CSS选择器灵活高效 ERROR_MSG_SPAN (By.CLASS_NAME, alert-error) # 2. 页面操作Actions封装对页面的操作行为 def enter_username(self, username): 输入用户名 self.clear_and_send_keys(self.USERNAME_INPUT, username) def enter_password(self, password): 输入密码 self.clear_and_send_keys(self.PASSWORD_INPUT, password) def click_login(self): 点击登录按钮 self.click(self.LOGIN_BUTTON) def get_error_message(self): 获取错误提示信息 return self.get_text(self.ERROR_MSG_SPAN) # 3. 业务场景组合可选将常用操作流封装成高级方法 def login(self, username, password): 完整的登录流程 self.enter_username(username) self.enter_password(password) self.click_login()为什么选择这些定位方式ID唯一且稳定渲染即确定是首选。但现代前端框架如React, Vue可能生成动态ID需注意。Name常用于表单元素也比较稳定。CSS Selector功能强大速度快支持复杂关系定位如div.content input:first-child。比XPath在大多数浏览器中性能更优。XPath万不得已时使用。它功能最强大但性能相对较差且容易因DOM结构微小变动而失效。仅在元素没有ID、Name、Class且CSS无法精确定位时使用尽量用相对路径如//button[text()提交]而非绝对路径。3.2 等待机制稳定性的生命线UI自动化测试失败十有八九是因为“等待”没做好。Selenium提供了几种等待方式隐式等待Implicit Waitdriver.implicitly_wait(10)。这是一个全局设置告诉WebDriver在查找任何元素时如果元素没有立即出现最多等待10秒。它只对find_element和find_elements方法生效。缺点它不关心元素是否处于“可交互状态”如可点击、可见。通常作为一道基础保险。显式等待Explicit Wait这是最推荐、最核心的等待策略。它允许你为某个特定的条件设置等待条件满足则立即继续超时则抛出异常。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待元素可见并可点击 wait WebDriverWait(driver, 10) # 最长等10秒 element wait.until(EC.element_to_be_clickable((By.ID, submit-btn))) element.click() # 等待元素包含特定文本 wait.until(EC.text_to_be_present_in_element((By.CLASS_NAME, status), 加载完成))常用Expected ConditionsECpresence_of_element_located: 元素存在于DOM树不一定可见。visibility_of_element_located: 元素可见宽高大于0。element_to_be_clickable: 元素可见且可点击。这是点击操作前的最佳等待条件。invisibility_of_element_located: 元素不可见或从DOM中移除常用于等待加载动画消失。强制等待time.sleepimport time; time.sleep(5)。这是最不推荐的方式因为它无条件固定等待浪费执行时间且无法自适应网络或机器性能。仅在调试脚本或处理极特殊、无任何状态可判断的场景时临时使用。在BasePage中的最佳实践将显式等待封装到基类的每个基础操作中。# common/base_page.py class BasePage: def __init__(self, driver): self.driver driver self.wait WebDriverWait(driver, 10) # 定义显式等待对象 def click(self, locator): 点击元素封装了等待 element self.wait.until(EC.element_to_be_clickable(locator)) element.click() def send_keys(self, locator, text): 输入文本封装了等待和清空 element self.wait.until(EC.visibility_of_element_located(locator)) element.clear() element.send_keys(text) def get_text(self, locator): 获取元素文本 element self.wait.until(EC.visibility_of_element_located(locator)) return element.text这样在Page Object中调用self.click(self.LOGIN_BUTTON)时就已经内置了“等待可点击”的逻辑脚本稳定性大幅提升。4. 积载系统核心测试场景实现现在我们进入正题看看如何测试积载系统的一个核心功能创建并计算一个积载计划。4.1 场景分析与步骤拆解假设我们的积载计划页面包含以下步骤从导航菜单进入“积载计划”模块。点击“新建计划”按钮。在表单中输入计划名称、选择船舶、输入航次。添加货物选择货类、输入重量、体积。点击“智能计算”按钮系统进行配载优化。验证计算结果检查总重量、重心、稳性等指标是否在预期范围内。保存计划。4.2 Page Object 实现StowagePlanPage# page_objects/stowage_plan_page.py from selenium.webdriver.common.by import By from selenium.webdriver.support.select import Select from common.base_page import BasePage import time class StowagePlanPage(BasePage): # 定位器 NAV_STOWAGE_LINK (By.LINK_TEXT, 积载计划) CREATE_NEW_BTN (By.ID, create-new-plan) PLAN_NAME_INPUT (By.ID, planName) SHIP_SELECT (By.ID, shipSelect) VOYAGE_INPUT (By.ID, voyage) # 货物行是动态添加的定位第一行的模板 CARGO_TYPE_SELECT (By.CSS_SELECTOR, select.cargo-type:first-of-type) CARGO_WEIGHT_INPUT (By.CSS_SELECTOR, input.cargo-weight:first-of-type) CARGO_VOLUME_INPUT (By.CSS_SELECTOR, input.cargo-volume:first-of-type) ADD_CARGO_BTN (By.ID, add-cargo-row) CALCULATE_BTN (By.ID, calculate-btn) SAVE_PLAN_BTN (By.ID, save-plan) # 结果区域 TOTAL_WEIGHT_SPAN (By.ID, totalWeight) CENTER_OF_GRAVITY_SPAN (By.ID, centerOfGravity) STABILITY_INDICATOR (By.ID, stabilityStatus) def navigate_to_stowage(self): 导航到积载计划页面 self.click(self.NAV_STOWAGE_LINK) # 可以增加一个等待确保页面加载完成例如等待“新建计划”按钮出现 self.wait.until(EC.visibility_of_element_located(self.CREATE_NEW_BTN)) def create_new_plan(self): 点击新建计划 self.click(self.CREATE_NEW_BTN) def fill_basic_info(self, plan_name, ship_name, voyage): 填写计划基本信息 self.send_keys(self.PLAN_NAME_INPUT, plan_name) # 处理下拉框 ship_select_element self.wait.until(EC.presence_of_element_located(self.SHIP_SELECT)) select Select(ship_select_element) select.select_by_visible_text(ship_name) # 根据文本选择 self.send_keys(self.VOYAGE_INPUT, voyage) def add_cargo_item(self, cargo_type, weight, volume): 添加一条货物信息 # 选择货类 cargo_type_select self.wait.until(EC.presence_of_element_located(self.CARGO_TYPE_SELECT)) Select(cargo_type_select).select_by_value(cargo_type) # 根据value选择 # 输入重量和体积 self.send_keys(self.CARGO_WEIGHT_INPUT, str(weight)) self.send_keys(self.CARGO_VOLUME_INPUT, str(volume)) # 点击“添加”按钮增加一行 self.click(self.ADD_CARGO_BTN) # 添加后等待新的一行渲染完成可以简单等待一小会儿或者等待某个元素出现 time.sleep(0.5) # 这里简化处理理想情况是等待新增行的某个元素出现 def perform_calculation(self): 点击智能计算按钮 self.click(self.CALCULATE_BTN) # 关键等待计算完成。通常系统会有加载状态这里假设计算完成后结果区域的文本会更新 self.wait.until(EC.text_to_be_present_in_element(self.TOTAL_WEIGHT_SPAN, 0)) # 等待总重量不再是初始值‘0’或‘--’实际应根据业务逻辑调整等待条件 def get_calculation_results(self): 获取计算结果 total_weight self.get_text(self.TOTAL_WEIGHT_SPAN) cog self.get_text(self.CENTER_OF_GRAVITY_SPAN) stability self.get_text(self.STABILITY_INDICATOR) return { total_weight: float(total_weight.replace(t, )), # 去除单位 center_of_gravity: cog, stability: stability } def save_plan(self): 保存计划 self.click(self.SAVE_PLAN_BTN) # 等待保存成功提示例如一个Toast消息 # self.wait.until(EC.visibility_of_element_located((By.CLASS_NAME, toast-success)))4.3 测试用例编写使用Pytest# test_cases/test_stowage_plan.py import pytest from page_objects.login_page import LoginPage from page_objects.stowage_plan_page import StowagePlanPage class TestStowagePlan: 积载计划功能测试 pytest.fixture(scopefunction) def login(self, driver): 每个测试用例前登录function级别的fixture login_page LoginPage(driver) login_page.navigate_to_login_page(http://your-system-url/login) # 假设基类有导航方法 login_page.login(valid_user, valid_password) yield # 如果需要可以在这里添加登出逻辑 def test_create_and_calculate_stowage_plan(self, driver, login): 测试创建积载计划并计算 # 1. 初始化页面对象 stowage_page StowagePlanPage(driver) # 2. 导航到积载计划页面 stowage_page.navigate_to_stowage() # 3. 创建新计划 stowage_page.create_new_plan() # 4. 填写基本信息 stowage_page.fill_basic_info(自动化测试计划-001, 东方之星号, VY2024001) # 5. 添加货物这里添加两种货物 stowage_page.add_cargo_item(container, 500, 1200) stowage_page.add_cargo_item(bulk_grain, 3000, 8500) # 6. 执行计算 stowage_page.perform_calculation() # 7. 验证计算结果 results stowage_page.get_calculation_results() # 断言总重量应在合理范围内3500 ± 50 expected_min_weight 3450 expected_max_weight 3550 assert expected_min_weight results[total_weight] expected_max_weight, \ f总重量{results[total_weight]}不在预期范围[{expected_min_weight}, {expected_max_weight}]内 # 断言稳性状态应为“良好” assert results[stability] 良好, f稳性状态异常: {results[stability]} # 8. 保存计划可选根据业务决定是否在测试中执行保存 # stowage_page.save_plan() # 可以添加更多测试用例如测试边界值、异常数据等 def test_calculate_with_empty_cargo(self, driver, login): 测试不添加货物直接计算应报错或结果为0 stowage_page StowagePlanPage(driver) stowage_page.navigate_to_stowage() stowage_page.create_new_plan() stowage_page.fill_basic_info(空货物测试, 东方之星号, VY2024002) # 不添加货物直接计算 stowage_page.perform_calculation() results stowage_page.get_calculation_results() assert results[total_weight] 0, 空货物计算总重量应为05. 数据驱动与参数化测试硬编码的测试数据不利于维护和扩展。Pytest的pytest.mark.parametrize装饰器可以轻松实现数据驱动测试。5.1 使用Excel管理测试数据首先准备一个Excel文件test_data/stowage_cases.xlsx用例编号计划名称船舶航次货类1重量1体积1货类2重量2体积2预期总重量下限预期总重量上限预期稳性TC-001计划-标准东方之星VY001container5001200bulk_grain3000850034503550良好TC-002计划-超重巨轮号VY002steel80002000---79508050临界TC-003计划-轻货快艇VY003cotton2005000---180220优秀然后编写一个数据读取工具# common/data_reader.py import openpyxl import json import os def read_excel_to_dict(file_path, sheet_nameSheet1): 读取Excel文件返回字典列表 wb openpyxl.load_workbook(file_path, data_onlyTrue) ws wb[sheet_name] data [] headers [cell.value for cell in next(ws.iter_rows(min_row1, max_row1))] for row in ws.iter_rows(min_row2, values_onlyTrue): row_dict dict(zip(headers, row)) # 过滤掉全为None的行 if any(row): data.append(row_dict) return data5.2 参数化测试用例# test_cases/test_stowage_plan_data_driven.py import pytest from page_objects.login_page import LoginPage from page_objects.stowage_plan_page import StowagePlanPage from common.data_reader import read_excel_to_dict # 从Excel读取测试数据 TEST_DATA_FILE os.path.join(os.path.dirname(__file__), ../test_data/stowage_cases.xlsx) test_cases read_excel_to_dict(TEST_DATA_FILE) class TestStowagePlanDataDriven: pytest.fixture(scopeclass) def login_setup(self, driver): 整个测试类只登录一次class级别fixture login_page LoginPage(driver) login_page.navigate_to_login_page(http://your-system-url/login) login_page.login(valid_user, valid_password) yield # 登出清理 pytest.mark.parametrize(case_data, test_cases, ids[case[用例编号] for case in test_cases]) def test_stowage_plan_with_data(self, driver, login_setup, case_data): 使用外部数据驱动的积载计划测试 stowage_page StowagePlanPage(driver) stowage_page.navigate_to_stowage() stowage_page.create_new_plan() # 使用测试数据填充表单 stowage_page.fill_basic_info( case_data[计划名称], case_data[船舶], case_data[航次] ) # 动态添加货物这里简化假设最多两行货物 cargo_list [] if case_data[货类1]: cargo_list.append((case_data[货类1], case_data[重量1], case_data[体积1])) if case_data.get(货类2): # 使用get避免KeyError cargo_list.append((case_data[货类2], case_data[重量2], case_data[体积2])) for cargo_type, weight, volume in cargo_list: stowage_page.add_cargo_item(cargo_type, weight, volume) stowage_page.perform_calculation() results stowage_page.get_calculation_results() # 断言 assert case_data[预期总重量下限] results[total_weight] case_data[预期总重量上限], \ f总重量{results[total_weight]}不符合预期 assert results[stability] case_data[预期稳性], \ f稳性状态{results[stability]}不符合预期{case_data[预期稳性]}这样我们只需要维护Excel表格就能轻松添加、修改测试用例实现测试逻辑与数据的分离。6. 测试报告与日志记录测试执行完了清晰的结果输出至关重要。Pytest本身支持多种报告格式。6.1 生成HTML报告使用pytest-html插件可以生成直观的HTML报告。 首先在pytest.ini中配置# pytest.ini [pytest] addopts -v --htmlreports/report.html --self-contained-html testpaths test_cases python_files test_*.py python_classes Test* python_functions test_*--self-contained-html参数会将CSS和JS内联到HTML文件中生成单个文件便于分享。执行测试后打开reports/report.html即可查看包含通过率、失败详情、执行时间等信息的报告。6.2 集成Allure报告更美观强大Allure报告提供了更丰富的展示包括步骤详情、附件截图、分类、趋势图等。安装Allure命令行工具需单独安装可从官网下载。运行测试时添加参数pytest --alluredir./reports/allure-results生成并打开报告allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report6.3 日志记录良好的日志能帮助快速定位问题。我们可以配置一个简单的日志模块。# common/logger.py import logging import os from datetime import datetime def setup_logger(nameui_auto_test, log_levellogging.INFO): 配置并返回一个logger实例 # 创建logger logger logging.getLogger(name) logger.setLevel(log_level) # 避免重复添加handler if not logger.handlers: # 创建控制台handler ch logging.StreamHandler() ch.setLevel(log_level) # 创建文件handler按日期分割日志 log_dir logs os.makedirs(log_dir, exist_okTrue) log_file os.path.join(log_dir, fui_test_{datetime.now().strftime(%Y%m%d)}.log) fh logging.FileHandler(log_file, encodingutf-8) fh.setLevel(logging.DEBUG) # 文件日志记录更详细 # 定义格式 formatter logging.Formatter( %(asctime)s - %(name)s - %(levelname)s - %(message)s ) ch.setFormatter(formatter) fh.setFormatter(formatter) # 添加handler到logger logger.addHandler(ch) logger.addHandler(fh) return logger # 在conftest.py或BasePage中全局使用 logger setup_logger()在BasePage的操作中嵌入日志def click(self, locator): 点击元素封装了等待和日志 logger.info(f尝试点击元素: {locator}) try: element self.wait.until(EC.element_to_be_clickable(locator)) element.click() logger.info(f成功点击元素: {locator}) except Exception as e: logger.error(f点击元素失败: {locator}, 错误信息: {e}) # 这里可以附加截图 self._take_screenshot(click_failed) raise7. 常见问题排查与稳定性提升技巧即使框架设计得再好在实际运行中也会遇到各种“坑”。下面是我在积载系统自动化测试中总结的常见问题及解决方案。7.1 元素定位失败NoSuchElementException这是最常见的问题。原因1页面未加载完成/元素未出现。解决使用显式等待WebDriverWaitEC而不是time.sleep或仅靠隐式等待。确保等待的条件是准确的如元素可点击、可见。原因2元素在iframe或shadow DOM内。解决使用driver.switch_to.frame(frame_element)切换到iframe内再进行定位。对于Shadow DOM需要使用JavaScript来穿透定位。# 切换到iframe iframe driver.find_element(By.TAG_NAME, iframe) driver.switch_to.frame(iframe) # 在iframe内操作... driver.switch_to.default_content() # 操作完切回主文档原因3元素是动态生成的定位器不稳定。解决避免使用绝对XPath或依赖固定索引的CSS。尝试使用更稳定的属性如># 不好的定位依赖固定结构 (By.XPATH, /html/body/div[3]/div[2]/div/div[2]/button[1]) # 更好的定位使用文本或属性 (By.XPATH, //button[contains(text(), 智能计算)]) (By.CSS_SELECTOR, button[data-rolecalculate])原因4页面有多个相同特征的元素。解决使用find_elements获取列表然后通过索引或过滤找到目标元素。确保你的定位器能唯一标识目标。7.2 元素交互失败ElementNotInteractableException元素找到了但点击或输入不生效。原因1元素被遮挡如弹窗、遮罩层。解决等待遮挡物消失或者使用JavaScript直接执行点击driver.execute_script(arguments[0].click();, element)。注意JS点击可能不会触发所有原生事件。原因2元素不在视口内。解决使用driver.execute_script(arguments[0].scrollIntoView(true);, element)将元素滚动到可视区域然后再操作。原因3元素状态不可交互如disabled。解决在操作前增加等待条件确保元素处于element_to_be_clickable状态。检查业务逻辑是否前置条件未满足导致按钮禁用。7.3 测试执行速度慢原因1过度使用time.sleep。解决全部替换为显式等待。显式等待在条件满足时会立刻继续最大程度减少等待时间。原因2网络或应用响应慢。解决适当增加显式等待的超时时间如从10秒加到30秒。考虑在测试环境中优化应用性能或使用更稳定的网络。原因3不必要的浏览器启动/关闭。解决使用Pytest的scopesession或scopeclass级别的fixture来复用浏览器实例而不是每个测试用例都重启浏览器。注意测试之间的状态隔离清理Cookies、LocalStorage。7.4 测试在CI/CD环境中不稳定Flaky Tests原因环境差异、资源竞争、 timing issue。解决策略增加重试机制使用pytest-rerunfailures插件对失败的测试自动重试几次。pip install pytest-rerunfailures运行pytest --reruns 3 --reruns-delay 2失败后重试3次每次间隔2秒使用更稳定的定位器与开发约定为关键测试元素添加># 在conftest.py中 import pytest pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: # 获取driver fixture假设它叫driver driver_fixture item.funcargs.get(driver) if driver_fixture: screenshot_path f./screenshots/failure_{item.name}_{datetime.now().strftime(%H%M%S)}.png driver_fixture.save_screenshot(screenshot_path) # 将截图路径附加到报告 report.extra [pytest_html.extras.image(screenshot_path, Failure Screenshot)]7.5 处理弹窗和浏览器通知积载系统可能会有各种浏览器弹窗alert, confirm, prompt或通知。# 等待并处理JavaScript Alert from selenium.webdriver.common.alert import Alert alert Alert(driver) print(alert.text) # 获取弹窗文本 alert.accept() # 点击“确定” # alert.dismiss() # 点击“取消” # alert.send_keys(input text) # 向prompt输入文本 # 处理浏览器通知需要在Options中提前设置 chrome_options.add_experimental_option(prefs, { profile.default_content_setting_values.notifications: 2 # 1-允许2-阻止 })8. 持续集成CI集成示例将自动化测试集成到CI/CD流水线中才能实现其最大价值。这里以GitHub Actions为例展示一个简单的配置。# .github/workflows/ui-test.yml name: UI Automation Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest # 使用Linux runner steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.10 - name: Install system dependencies (for Chrome) run: | sudo apt-get update sudo apt-get install -y wget unzip # 安装Chrome wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - echo deb [archamd64] http://dl.google.com/linux/chrome/deb/ stable main | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run UI Tests with Headless Chrome env: # 可以在这里设置测试环境URL等变量 BASE_URL: ${{ secrets.TEST_BASE_URL }} run: | # 使用pytest运行测试生成多种报告 pytest -v \ --htmlreports/report.html \ --self-contained-html \ --alluredirreports/allure-results \ --reruns 2 \ --reruns-delay 1 - name: Upload HTML Report uses: actions/upload-artifactv3 if: always() # 即使测试失败也上传报告 with: name: ui-test-html-report path: reports/report.html - name: Upload Allure Results uses: actions/upload-artifactv3 if: always() with: name: allure-results path: reports/allure-results这个工作流会在代码推送或拉取请求时自动触发在Ubuntu环境中安装依赖、运行测试并将测试报告作为制品保存供后续查看。构建一个健壮的Selenium UI自动化测试框架远不止是写几个find_element和click。它涉及到环境治理、架构设计、等待策略、数据管理、报告生成和CI/CD集成等一系列工程化实践。对于像积载系统这样复杂的业务应用一个考虑周全的框架是测试脚本能够长期稳定运行、高效维护的根本保障。在实际项目中还需要根据团队规范、技术栈和业务特点进行灵活调整例如引入Page Factory模式、BDDBehave框架、或者容器化测试环境等。希望这个基于真实项目的完整实例能为你提供一条清晰的实践路径。