从零构建Python+Selenium自动化测试框架:PO模式与数据驱动实践

从零构建Python+Selenium自动化测试框架:PO模式与数据驱动实践
1. 项目概述为什么需要一个自己的自动化测试框架干了这么多年测试从手工点点点到写脚本再到搭建和维护团队级的自动化测试平台我最大的感触是对于大多数中小型项目或者个人学习而言一个“大而全”的商用或开源平台往往过于沉重。配置复杂、学习曲线陡峭很多高级功能你可能根本用不上反而被各种依赖和环境问题搞得焦头烂额。相反一个自己亲手搭建、结构清晰、量身定制的轻量级框架才是解决问题的利器。这个“简简单单实现一个PythonSelenium的自动化测试框架”的项目核心目标就是帮你摆脱对复杂工具的依赖用最主流的Python语言和Selenium库从零构建一个属于你自己的、可维护、可扩展的Web UI自动化测试脚手架。它不追求功能的面面俱到而是强调实用性和可理解性。你会清晰地知道每一行代码的作用每一个目录存在的意义当测试用例失败时你能快速定位是页面元素变了、测试数据错了还是业务流程的逻辑有问题。为什么是PythonSeleniumPython语法简洁生态丰富是自动化测试领域的绝对主流Selenium则是Web UI自动化的行业标准支持所有主流浏览器。这个组合能确保你的技能具有高度的通用性和市场价值。通过这个项目你不仅能学会如何让浏览器自动执行点击、输入等操作更能掌握一个测试框架的核心设计思想如何组织代码、管理测试数据、生成报告以及处理各种异常。接下来我会带你一步步拆解这个框架的每一个模块分享我在搭建过程中踩过的坑和总结的最佳实践。2. 框架核心设计思路与选型考量在动手写代码之前花点时间思考框架的设计思路至关重要。一个好的设计能让你后续的维护成本降低80%。这里我们采用一种经典且高效的分层架构模式。2.1 为什么选择POPage Object模式这是本框架的基石。PO模式的核心思想是将页面定位元素和测试业务流程分离。想象一下如果所有元素的ID、XPath都直接散落在几十个测试用例脚本里一旦前端页面改了一个按钮的ID你就需要满世界去修改所有相关的测试脚本这无疑是场灾难。PO模式的做法是为每一个被测页面如登录页、主页、订单页创建一个对应的“页面对象类”。这个类里只做两件事定义元素定位器将所有这个页面上的按钮、输入框、链接等元素的查找方式如By.ID, By.XPATH封装成类属性。封装页面操作提供一系列方法比如login(username, password)方法内部会调用元素定位器并执行点击、输入等操作。这样测试用例脚本变得非常清爽它只关心业务逻辑“打开登录页 - 输入用户名密码 - 点击登录 - 验证是否跳转到首页”。至于“用户名输入框在哪”、“登录按钮怎么点”这些细节全部被封装在页面对象类里。前端页面变化时你通常只需要修改对应的一个页面对象类即可。2.2 数据驱动测试用YAML管理测试数据与元素将测试数据如登录用的手机号、密码和页面元素定位信息如元素ID从代码中剥离出来用YAML文件来管理这是提升框架可维护性的另一关键。YAML格式易读易写结构清晰。管理测试数据你可以为登录测试创建一个login_data.yaml里面用列表定义多组测试数据包括正确的、错误的、边界值等。测试框架通过数据驱动工具如ddt自动加载这些数据逐一执行用例。增加新测试场景只需在YAML文件里加一组数据无需改动代码。管理元素定位同样可以为登录页创建一个login.yaml里面存储所有元素的定位方式和操作类型。这样做有一个巨大的好处当元素定位符因前端微调而改变时比如ID从username变成了userName你无需去Python代码里搜索替换只需更新对应的YAML文件。这对于团队协作尤其友好测试人员可以独立维护元素定位文件。2.3 测试用例组织与执行unittest DDTPython自带的unittest模块提供了测试用例TestCase、测试套件TestSuite、断言Assert等完整结构是组织测试的标准方式。我们用它作为测试用例的骨架。DDTData-Driven Tests库与unittest完美结合它能方便地将我们从YAML或JSON中读取的测试数据“装饰”到测试方法上实现数据驱动。一个测试方法配合多组数据就能衍生出多个测试用例极大地减少了代码重复。2.4 目录结构设计清晰即高效一个混乱的目录是项目腐化的开始。我们采用如下清晰的分层结构这也是业界常见的实践your_autoframework/ ├── config/ │ ├── __init__.py │ └── setting.py # 全局配置文件路径、URL、浏览器类型等 ├── test_data/ │ ├── login_data.yaml # 登录测试数据 │ └── ... ├── test_page_element/ │ ├── login.yaml # 登录页面元素定位信息 │ └── ... ├── page_obj/ │ ├── __init__.py │ ├── base.py # 页面基类封装公共方法如find_element │ └── login_page.py # 登录页面对象类 ├── test_case/ │ ├── __init__.py │ └── test_login.py # 登录测试用例脚本 ├── public/ │ ├── __init__.py │ ├── models/ │ │ ├── __init__.py │ │ ├── myunit.py # 封装测试基类如setup/teardown │ │ ├── logger.py # 日志模块 │ │ └── screenshot.py # 截图模块 │ └── utils/ │ └── get_yaml.py # 读取YAML文件的工具函数 ├── reports/ │ ├── html_reports/ # 存放HTML测试报告 │ └── screenshots/ # 存放失败用例截图 └── run_main.py # 主程序用于执行全部用例并生成报告这样的结构让功能模块各司其职无论是寻找元素定义、查看测试数据还是编写新的页面操作都能迅速找到对应位置。3. 环境搭建与核心组件封装工欲善其事必先利其器。在开始构建框架主体前我们需要把基础环境和一些通用的“轮子”准备好。3.1 Python环境与Selenium安装确保你安装了Python 3.6及以上版本。使用pip安装必要的库pip install selenium pip install PyYAML pip install ddt pip install HTMLTestRunner-PY3 # 用于生成HTML测试报告注意HTMLTestRunner原版只支持Python 2务必安装HTMLTestRunner-PY3这个兼容Python 3的版本。接下来是浏览器驱动。以最常用的Chrome为例查看你本地Chrome浏览器的版本在地址栏输入chrome://version/。前往ChromeDriver官网或国内镜像站下载与之版本匹配或主要版本号相同的chromedriver。将下载的chromedriver.exeWindows或chromedriverMac/Linux放在一个目录下并将该目录添加到系统的PATH环境变量中。更简单的做法是直接把它放到Python的安装目录下和python.exe同级因为该目录通常已在PATH中。验证安装打开Python交互环境执行以下代码不报错且能打开浏览器即表示成功。from selenium import webdriver driver webdriver.Chrome() # 如果驱动不在PATH需指定路径webdriver.Chrome(executable_path‘你的路径/chromedriver’) driver.get(“http://www.baidu.com”) driver.quit()3.2 封装基础页面类BasePage这是所有页面对象类的父类封装了Selenium最常用的、与页面交互的基础操作。这样做可以避免在每个页面类中重复编写相同的代码也便于后期统一修改。# page_obj/base.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException import logging class BasePage: def __init__(self, driver): self.driver driver self.logger logging.getLogger(__name__) self.timeout 10 # 显式等待超时时间 def find_element(self, *locator): 查找单个元素加入显式等待 try: element WebDriverWait(self.driver, self.timeout).until( EC.presence_of_element_located(locator) ) return element except TimeoutException: self.logger.error(f查找元素超时: {locator}) raise except NoSuchElementException: self.logger.error(f未找到元素: {locator}) raise def find_elements(self, *locator): 查找多个元素 try: elements WebDriverWait(self.driver, self.timeout).until( EC.presence_of_all_elements_located(locator) ) return elements except TimeoutException: self.logger.error(f查找元素列表超时: {locator}) return [] # 返回空列表而不是抛出异常有时更友好 def click(self, *locator): 点击元素 element self.find_element(*locator) element.click() def send_keys(self, *locator, text): 向输入框输入文本先清空原有内容 element self.find_element(*locator) element.clear() element.send_keys(text) def get_text(self, *locator): 获取元素的文本内容 element self.find_element(*locator) return element.text def is_element_visible(self, *locator): 判断元素是否可见 try: element WebDriverWait(self.driver, 5).until( EC.visibility_of_element_located(locator) ) return True except TimeoutException: return False def switch_to_frame(self, *locator): 切换到iframe frame_element self.find_element(*locator) self.driver.switch_to.frame(frame_element) def switch_to_default_content(self): 切回主文档 self.driver.switch_to.default_content()实操心得在find_element方法中强制使用显式等待是至关重要的最佳实践。相比time.sleep()这种“傻等”显式等待会在指定时间内不断尝试查找元素一旦找到就立即返回大大提升了测试执行效率并避免了因网络或页面加载速度导致的随机失败。3.3 封装日志与截图功能日志和截图是自动化测试的“眼睛”是调试和复现问题不可或缺的工具。日志模块使用Python标准库logging进行封装可以输出到控制台和文件。# public/models/logger.py import logging import os from datetime import datetime def get_logger(nameAutoTest, log_levellogging.INFO): # 创建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 os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), logs) os.makedirs(log_dir, exist_okTrue) log_file os.path.join(log_dir, fautotest_{datetime.now().strftime(%Y%m%d)}.log) fh logging.FileHandler(log_file, encodingutf-8) fh.setLevel(log_level) # 定义输出格式 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截图模块在测试失败时自动截图能直观地看到出错时页面的状态。# public/models/screenshot.py import os from datetime import datetime def save_screenshot(driver, screenshot_nameNone): 保存截图到指定目录 :param driver: webdriver实例 :param screenshot_name: 截图文件名不包含扩展名 :return: 截图文件的完整路径 screenshot_dir os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), reports, screenshots) os.makedirs(screenshot_dir, exist_okTrue) if not screenshot_name: screenshot_name fscreenshot_{datetime.now().strftime(%H%M%S)} file_path os.path.join(screenshot_dir, f{screenshot_name}.png) driver.save_screenshot(file_path) return file_path在测试基类中集成我们需要创建一个所有测试用例类都继承的基类在这里统一处理浏览器的启动/关闭、日志初始化和失败截图。# public/models/myunit.py import unittest from selenium import webdriver from public.models.logger import get_logger from public.models.screenshot import save_screenshot class MyTest(unittest.TestCase): classmethod def setUpClass(cls): 整个测试类开始前执行一次 cls.logger get_logger() cls.logger.info( * 50) cls.logger.info(f开始执行测试类: {cls.__name__}) def setUp(self): 每个测试方法开始前执行 self.logger.info(f--- 开始测试用例: {self._testMethodName} ---) # 初始化浏览器这里以Chrome为例 options webdriver.ChromeOptions() options.add_argument(--ignore-certificate-errors) # 忽略证书错误 options.add_argument(--disable-gpu) # options.add_argument(--headless) # 无头模式不打开浏览器窗口 self.driver webdriver.Chrome(optionsoptions) self.driver.implicitly_wait(5) # 设置隐式等待 self.driver.maximize_window() def tearDown(self): 每个测试方法结束后执行 if hasattr(self, _outcome): # 判断测试结果 result self._outcome.result if result.errors or result.failures: # 测试失败截图 screenshot_path save_screenshot(self.driver, self._testMethodName) self.logger.error(f测试失败截图已保存至: {screenshot_path}) self.driver.quit() self.logger.info(f--- 结束测试用例: {self._testMethodName} ---\n) classmethod def tearDownClass(cls): 整个测试类结束后执行一次 cls.logger.info(f测试类 {cls.__name__} 执行完毕) cls.logger.info( * 50)踩坑提醒setUp/tearDown在每个用例前后都会执行这意味着每个用例都会打开和关闭一次浏览器。虽然这样保证了用例间的独立性但会拖慢整体速度。对于需要登录状态的串联用例可以考虑使用setUpClass/tearDownClass整个类只执行一次并在用例间手动清理cookie或使用self.addCleanup来确保状态隔离。这需要根据你的测试场景权衡。4. 核心模块实现从元素管理到用例执行基础打好后我们开始实现框架最核心的部分如何将YAML中定义的元素和数据通过页面对象和测试用例串联起来并最终执行。4.1 用YAML管理页面元素与测试数据首先我们定义登录页面的元素。文件保存在test_page_element/login.yaml。# login.yaml - 页面元素定位文件 # 每个元素是一个字典包含定位方式和值 login_elements: - name: “登录链接” locator: “id” value: “login-link-a” operate: “click” desc: “首页的登录按钮” - name: “手机号输入框” locator: “id” value: “mobile” operate: “send_keys” desc: “登录对话框中的手机号输入框” - name: “密码输入框” locator: “id” value: “mbpwd” operate: “send_keys” desc: “登录对话框中的密码输入框” - name: “登录按钮” locator: “xpath” value: “//span[text()‘登录’]” operate: “click” desc: “登录对话框中的提交按钮” - name: “登录成功提示” locator: “id” value: “userProNick” operate: “get_text” desc: “登录成功后显示用户名的元素” - name: “登录错误提示” locator: “xpath” value: “//div[class‘box-mobilelogin’]/div[1]/span” operate: “get_text” desc: “手机号或密码错误时的提示信息”然后定义测试数据。文件保存在test_data/login_data.yaml。# login_data.yaml - 测试数据文件 - test_id: “TC_LOGIN_001” description: “手机号和密码均为空登录” data: phone: “” password: “” expected: “手机号不能为空” screenshot: “login_empty” - test_id: “TC_LOGIN_002” description: “密码为空登录” data: phone: “13800138000” password: “” expected: “密码不能为空” screenshot: “login_pwd_empty” - test_id: “TC_LOGIN_003” description: “错误密码登录” data: phone: “13800138000” password: “wrong” expected: “账号密码错误” screenshot: “login_wrong_pwd” - test_id: “TC_LOGIN_004” description: “正确手机号和密码登录” data: phone: “13800138000” # 请替换为有效的测试账号 password: “your_password” expected: “欢迎User” # 请替换为登录成功后的实际提示 screenshot: “login_success”4.2 编写YAML读取工具我们需要一个通用的工具来读取这些YAML文件。# public/utils/get_yaml.py import yaml import os from public.models.logger import get_logger logger get_logger() class GetYaml: def __init__(self, file_path): 初始化读取yaml文件 :param file_path: yaml文件绝对路径 if os.path.exists(file_path): self.file_path file_path with open(file_path, r, encodingutf-8) as f: self.data yaml.safe_load(f) # 使用safe_load更安全 else: logger.error(fYAML文件不存在: {file_path}) self.data None def get_data(self, keyNone): 获取yaml文件中的数据 :param key: 字典的key为None时返回全部数据 :return: 对应的值 if self.data is None: return None if key is None: return self.data # 支持通过点号分隔的嵌套键如 ‘login_elements.0.name’ keys key.split(‘.’) value self.data try: for k in keys: if isinstance(value, list) and k.isdigit(): value value[int(k)] else: value value[k] return value except (KeyError, IndexError, TypeError): logger.warning(f”在YAML文件 {self.file_path} 中未找到键: {key}“) return None # 示例用法 if __name__ ‘__main__’: # 假设文件路径 element_loader GetYaml(‘test_page_element/login.yaml’) all_elements element_loader.get_data(‘login_elements’) first_element_name element_loader.get_data(‘login_elements.0.name’) print(first_element_name) # 输出登录链接4.3 实现页面对象类LoginPage现在利用BasePage和YAML工具创建登录页面对象类。# page_obj/login_page.py from selenium.webdriver.common.by import By from public.page_obj.base import BasePage from public.utils.get_yaml import GetYaml import os # 获取元素定位文件的绝对路径这里需要根据你的项目结构调整 # 一种好的实践是在config/setting.py中定义所有路径常量 BASE_DIR os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) ELEMENT_FILE os.path.join(BASE_DIR, ‘test_page_element’, ‘login.yaml’) class LoginPage(BasePage): 登录页面对象 # 加载元素定位数据 _element_loader GetYaml(ELEMENT_FILE) _elements _element_loader.get_data(‘login_elements’) # 通过元素名称映射到定位器和操作类型 # 这里简化处理实际可以根据yaml中的‘name’字段动态生成 # 为了清晰我们手动映射一下实际项目可优化为自动解析 property def login_link(self): elem_info self._elements[0] # ‘登录链接’ return (getattr(By, elem_info[‘locator’].upper()), elem_info[‘value’]) property def phone_input(self): elem_info self._elements[1] # ‘手机号输入框’ return (getattr(By, elem_info[‘locator’].upper()), elem_info[‘value’]) property def password_input(self): elem_info self._elements[2] # ‘密码输入框’ return (getattr(By, elem_info[‘locator’].upper()), elem_info[‘value’]) property def submit_button(self): elem_info self._elements[3] # ‘登录按钮’ return (getattr(By, elem_info[‘locator’].upper()), elem_info[‘value’]) property def success_indicator(self): elem_info self._elements[4] # ‘登录成功提示’ return (getattr(By, elem_info[‘locator’].upper()), elem_info[‘value’]) property def error_hint(self): elem_info self._elements[5] # ‘登录错误提示’ return (getattr(By, elem_info[‘locator’].upper()), elem_info[‘value’]) # 页面操作方法 def open_login_page(self, url): 打开登录页面假设从首页点击登录链接进入 self.driver.get(url) self.click(*self.login_link) # 点击登录链接弹出对话框 def input_phone(self, phone): 输入手机号 self.send_keys(*self.phone_input, textphone) def input_password(self, pwd): 输入密码 self.send_keys(*self.password_input, textpwd) def click_submit(self): 点击登录按钮 self.click(*self.submit_button) def get_error_message(self): 获取错误提示信息 return self.get_text(*self.error_hint) def get_success_message(self): 获取登录成功后的欢迎信息 return self.get_text(*self.success_indicator) # 完整的登录业务流程封装 def login(self, phone, password): 登录流程组合方法 self.logger.info(f”尝试登录用户名: {phone}“) self.input_phone(phone) self.input_password(password) self.click_submit()设计解析这里采用了属性property来封装定位器。好处是每次访问self.phone_input时它返回的都是一个实时的定位元组(By.ID, ‘mobile’)。我们将具体的定位信息ID、XPath与操作方法的代码完全解耦。如果前端元素变了只需更新login.yaml文件这里的代码一行都不用动。login方法是对多个单步操作的高层封装让测试用例脚本更加简洁。4.4 编写数据驱动的测试用例最后我们使用unittest和ddt来编写测试用例。# test_case/test_login.py import unittest import os import sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # 将项目根目录加入PATH from ddt import ddt, data, unpack from public.models.myunit import MyTest from public.page_obj.login_page import LoginPage from public.utils.get_yaml import GetYaml from public.models.screenshot import save_screenshot # 加载测试数据 DATA_FILE os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ‘test_data’, ‘login_data.yaml’) test_data_loader GetYaml(DATA_FILE) test_cases test_data_loader.get_data() # 获取所有测试用例数据 ddt class TestLogin(MyTest): # 继承自定义的测试基类 “”“登录功能测试”“” data(*test_cases) # 使用ddt解包测试数据 unpack def test_login(self, test_id, description, data, expected, screenshot): “”“登录测试主流程”“” self.logger.info(f”执行用例: {test_id} - {description}“) # 1. 初始化页面对象 login_page LoginPage(self.driver) # 2. 打开被测网站这里需要替换成你的实际登录页URL # 注意这里假设登录对话框在首页点击后弹出。根据实际情况调整。 base_url “http://your-test-site.com” login_page.open_login_page(base_url) # 3. 执行登录操作 phone data.get(‘phone’) password data.get(‘password’) login_page.login(phone, password) # 4. 断言验证 # 根据预期结果判断是成功还是失败场景 if ‘成功’ in description or ‘正确’ in description: # 成功场景验证成功提示信息 actual_text login_page.get_success_message() # 断言前等待一下确保元素加载出来 self.assertTrue(login_page.is_element_visible(*login_page.success_indicator), f”登录成功提示元素未找到用例{test_id}可能失败“) self.assertEqual(actual_text, expected, f”登录成功提示不符。预期: ‘{expected}’ 实际: ‘{actual_text}’“) self.logger.info(f”用例 {test_id} 断言成功登录成功。“) else: # 失败场景验证错误提示信息 actual_text login_page.get_error_message() self.assertEqual(actual_text, expected, f”错误提示信息不符。预期: ‘{expected}’ 实际: ‘{actual_text}’“) self.logger.info(f”用例 {test_id} 断言成功符合预期的错误提示。“) # 5. 无论成功失败都截图成功截图可用于报告失败截图已在tearDown中捕获 save_screenshot(self.driver, f”{test_id}_{screenshot}“) if __name__ ‘__main__’: unittest.main()这段代码清晰地展示了数据驱动测试的威力。我们只有一个测试方法test_login但通过data(*test_cases)装饰器ddt会自动为test_data.yaml中的每一组数据生成一个独立的测试用例来执行。测试逻辑打开页面、输入、点击、断言是通用的测试数据和预期结果来自外部文件实现了脚本与数据的彻底分离。5. 测试执行、报告生成与高级技巧框架的主体已经完成现在我们需要一个“总控”程序来批量执行测试用例并生成一份漂亮的测试报告。同时分享一些让框架更稳健、更实用的高级技巧。5.1 构建测试执行主程序主程序run_main.py负责发现、加载、运行所有测试用例并生成HTML报告。# run_main.py import unittest import time import os import sys from HTMLTestRunner_PY3 import HTMLTestRunner # 将项目根目录加入系统路径确保模块导入正常 project_root os.path.dirname(os.path.abspath(__file__)) sys.path.append(project_root) def discover_tests(test_dir‘./test_case’, pattern‘test_*.py’): “”“发现指定目录下的所有测试用例”“” discover unittest.defaultTestLoader.discover(start_dirtest_dir, patternpattern, top_level_dirNone) return discover def run_and_generate_report(test_suite, report_dir‘./reports/html_reports’): “”“执行测试并生成HTML报告”“” # 确保报告目录存在 os.makedirs(report_dir, exist_okTrue) # 生成带时间戳的报告文件名 now time.strftime(“%Y-%m-%d_%H-%M-%S”) report_filename os.path.join(report_dir, f”TestReport_{now}.html”) with open(report_filename, ‘wb’) as f: runner HTMLTestRunner(streamf, title‘Web自动化测试报告’, description‘测试环境Chrome浏览器’, tester‘你的名字’) runner.run(test_suite) print(f”测试报告已生成: {report_filename}“) return report_filename if __name__ ‘__main__’: # 1. 发现测试用例 all_tests discover_tests() if all_tests.countTestCases() 0: print(“未发现任何测试用例请检查test_case目录和文件命名模式(test_*.py)。“) else: print(f”共发现 {all_tests.countTestCases()} 个测试用例。“) # 2. 执行并生成报告 run_and_generate_report(all_tests)运行python run_main.py脚本会自动查找test_case目录下所有以test_开头的Python文件执行其中的测试用例并在reports/html_reports目录下生成一个带有时间戳的HTML报告。报告里会清晰列出每个用例的执行结果通过/失败/错误、耗时、以及详细的日志和错误堆栈信息。5.2 处理常见的“坑”与不稳定因素Web UI自动化测试天生比接口测试更“脆弱”因为依赖浏览器、网络和前端渲染。以下是我总结的几个关键应对策略1. 元素定位策略与等待机制优先使用ID和Name它们通常最稳定。其次是CSS Selector比XPath性能更好、更易读。XPath虽然强大但容易因前端结构微调而失效应谨慎使用。显式等待是黄金法则如前所述用WebDriverWait配合expected_conditions如element_to_be_clickable,visibility_of_element_located替代time.sleep()和隐式等待。为不同的操作设置合理的超时时间。重试机制对于某些偶发性失败如点击没反应可以在封装的方法里加入简单的重试逻辑。def click_with_retry(self, *locator, retries2): for attempt in range(retries): try: self.click(*locator) return True except ElementClickInterceptedException: self.logger.warning(f”点击 {locator} 被拦截第{attempt1}次重试...“) time.sleep(1) raise ElementClickInterceptedException(f”点击 {locator} 失败重试{retries}次后仍被拦截。“)2. 测试数据与环境隔离使用测试账号绝对不要使用生产环境的真实用户账号进行自动化测试。准备专用的测试账号并确保测试数据如新注册的用户、新建的订单可以被安全地创建和清理。清理测试痕迹对于会改变系统状态的测试如创建订单在tearDown中编写清理逻辑或者定期由后台任务清理测试数据避免数据堆积影响后续测试。3. 截图与日志的精准记录除了在tearDown中全局捕获失败截图也可以在关键检查点如登录前后、提交订单前后主动截图方便回溯。日志级别要合理设置。调试时用DEBUG运行时用INFO错误用ERROR。在日志信息中尽量包含有意义的上下文如“正在登录用户test_user_001”。4. 浏览器兼容性与无头模式框架应能方便地切换浏览器。可以通过配置文件或命令行参数指定浏览器类型Chrome, Firefox, Edge。在CI/CD管道中运行时通常使用无头模式--headless以节省资源。在myunit.py的setUp中可以通过选项控制。# config/setting.py BROWSER ‘chrome’ # 可配置为 ‘firefox’, ‘edge’ HEADLESS True # 是否启用无头模式 # myunit.py 修改setUp部分 from config import setting options webdriver.ChromeOptions() if setting.HEADLESS: options.add_argument(‘--headless’) options.add_argument(‘--disable-gpu’) options.add_argument(‘--window-size1920,1080’) # 无头模式建议设置窗口大小 self.driver webdriver.Chrome(optionsoptions)5.3 框架的扩展方向这个基础框架已经具备了核心功能你可以根据项目需求轻松扩展集成API测试在public目录下增加api_client模块封装HTTP请求。测试用例可以混合调用UI操作和API请求实现更复杂的场景如先用API准备数据再用UI验证展示。数据库校验引入pymysql或sqlalchemy等库在断言时不仅检查页面还可以直接查询数据库验证数据一致性。测试报告增强除了HTMLTestRunner可以集成更强大的Allure报告框架生成交互性更强、信息更丰富的测试报告。配置化管理将数据库连接信息、不同环境的URL、邮件发送配置等全部放入config目录下的配置文件中如config.ini或settings.yaml实现一套代码多环境运行。并发执行使用pytest-xdist或unittest的TestSuite组合可以实现测试用例的并发执行大幅缩短测试总时间。搭建自己的自动化测试框架就像打造一件称手的兵器。开始时可能觉得繁琐但一旦建成你会发现它对测试效率和软件质量的提升是巨大的。这个过程中最重要的不是死记硬背代码而是理解其背后的设计思想分层、封装、数据驱动。掌握了这些无论未来面对多么复杂的系统你都能快速设计出适合的自动化测试方案。希望这个“简简单单”的框架能成为你自动化测试之旅的一个坚实起点。