Python Selenium 自动化测试框架搭建与实战TPshop 商城为例本文面向初学者通过一个完整的 TPshop 商城后台自动化测试项目拆解如何搭建 Selenium 自动化框架。所有代码注释均在代码下方帮你彻底理解每一句代码的作用。本文仅供学习笔记使用不涉及商业用途。 目录一、项目结构总览二、核心模块逐行详解2.1utils.py—— 驱动工厂与通用等待2.2config.py—— 全局配置与日志2.3base_page.py—— 基础页面类2.4 页面对象层Page Object2.5 测试用例 —— 登录并新增商品三、新手最容易踩的坑四、总结一、项目结构总览project/ ├── base/ │ └── base_page.py # 基础页面类封装通用操作 ├── config.py # 全局配置路径、日志、数据读取 ├── utils.py # 驱动管理、自定义工具函数 ├── script/ │ └── admin/ │ ├── login_page.py # 登录页面对象 │ ├── admin_home.py # 后台主页对象 │ └── admin_goods.py # 商品管理页面对象 └── test_goods.py # 测试用例登录并新增商品二、核心模块逐行详解2.1utils.py—— 驱动工厂与通用等待importloggingimportrefromselenium.webdriver.support.waitimportWebDriverWaitfromselenium.webdriver.common.byimportBy# 导入日志模块正则模块Selenium的显式等待和定位方式classDriverUtils:# 定义三个私有类变量分别保存买家、后台、APP的驱动对象初始为空__buyer_driverNone# 买家端浏览器驱动__admin_driverNone# 后台管理系统浏览器驱动__app_driverNone# APP端驱动预留classmethoddefget_buyer_driver(cls):# 获取买家端的浏览器驱动如果还没有就创建一个新的ifcls.__buyer_driverisNone:# 如果驱动对象不存在fromseleniumimportwebdriver# 动态导入webdrivercls.__buyer_driverwebdriver.Edge()# 创建Edge浏览器实例cls.__buyer_driver.maximize_window()# 窗口最大化cls.__buyer_driver.implicitly_wait(5)# 设置隐式等待5秒# 访问买家端首页替换为你自己的地址cls.__buyer_driver.get(http://192.168.1.161/index.php/Home/Index/index)returncls.__buyer_driver# 返回驱动对象classmethoddefquit_buyer_driver(cls):# 关闭并清理买家端驱动ifcls.__buyer_driverisnotNone:# 如果驱动对象存在cls.__buyer_driver.quit()# 关闭浏览器cls.__buyer_driverNone# 将变量重置为空classmethoddefget_admin_driver(cls):# 获取后台管理系统的浏览器驱动ifcls.__admin_driverisNone:fromseleniumimportwebdriver cls.__admin_driverwebdriver.Edge()cls.__admin_driver.maximize_window()cls.__admin_driver.implicitly_wait(5)# 访问后台登录页cls.__admin_driver.get(http://192.168.1.161/index.php/Admin/Admin/login)returncls.__admin_driverclassmethoddefquit_admin_driver(cls):# 关闭并清理后台驱动ifcls.__admin_driverisnotNone:cls.__admin_driver.quit()cls.__admin_driverNoneclassmethoddefget_app_driver(cls):# 获取APP驱动目前尚未实现调用会报错提醒ifcls.__app_driverisNone:raiseNotImplementedError(APP驱动尚未实现)# 抛出未实现异常returncls.__app_driverclassmethoddefquit_app_driver(cls):# 关闭APP驱动ifcls.__app_driverisnotNone:cls.__app_driver.quit()cls.__app_driverNonedefel_is_exist_by_text(driver,key_text):# 根据页面文本判断元素是否存在存在返回元素对象不存在返回False并截图safe_textkey_text.replace(,\\)# 转义双引号防止XPath语法错误try:# 显式等待直到找到包含指定文本的元素is_sucWebDriverWait(driver,10,1).until(lambdax:x.find_element(By.XPATH,f//*[text(){safe_text}]))exceptExceptionase:is_sucFalse# 没找到则返回False# 清理文件名中的非法字符例如 / \ : * ? |safe_namere.sub(r[\\/*?:|],_,key_text)driver.get_screenshot_as_file(f{safe_name}_未找到.png)# 失败截图logging.error(f未找到文本为{key_text}的元素对象!)returnis_suc# 返回查找结果defget_el_text(driver,xpath_str):# 根据xpath获取元素的文本内容msgNonetry:# 显式等待定位元素并获取其text属性msgWebDriverWait(driver,10,1).until(lambdax:x.find_element(By.XPATH,xpath_str)).text logging.info(msg)# 用日志输出文本内容exceptExceptionase:logging.error(f没有获取到{xpath_str}的元素对象文本!)msgNonereturnmsg 重点笔记使用类属性__buyer_driver实现单例模式整个测试过程只有一个浏览器窗口节省资源。implicitly_wait(5)是隐式等待全局生效WebDriverWait是显式等待仅对当次查找生效。截图文件名要避免特殊字符否则会导致保存失败。2.2config.py—— 全局配置与日志importosimportjsonimportloggingfromlogging.handlersimportTimedRotatingFileHandlerfrompathlibimportPath# 导入系统路径、JSON、日志、日志切割处理器、现代路径库# 获取当前文件所在目录作为项目根目录BASE_PATHPath(__file__).resolve().parentdefbuild_data(file_name):# 读取JSON测试数据返回二维列表供参数化使用filepathos.path.join(BASE_PATH,data,f{file_name}.json)# 拼接完整路径case_data[]# 存放所有用例数据try:withopen(filepath,encodingutf-8)asf:all_datajson.load(f)# 读取JSON转为字典except(FileNotFoundError,json.JSONDecodeError)ase:logging.error(f读取数据文件失败:{filepath}-{e})raise# 重新抛出异常foriinall_data.values():# 遍历字典的每个值也是字典case_data.append(list(i.values()))# 提取值转为列表加入结果returncase_datadefbasic_log_config():# 配置日志同时输出到文件和控制台loggerlogging.getLogger()# 获取根日志器iflogger.handlers:# 防止重复添加处理器returnlogger.setLevel(logging.INFO)# 设置全局日志级别# 创建一个按天切割的日志文件处理器保留2个备份lhtTimedRotatingFileHandler(filenameos.path.join(BASE_PATH,log,tp_test.log),# 日志文件路径whenmidnight,# 每天午夜切割interval1,# 间隔1天backupCount2,# 只保留最近2个备份encodingutf8)lslogging.StreamHandler()# 输出到控制台formatterlogging.Formatter(fmt%(asctime)s %(levelname)s [%(filename)s(%(funcName)s:%(lineno)d)] - %(message)s)lht.setFormatter(formatter)# 设置格式化器ls.setFormatter(formatter)logger.addHandler(lht)# 添加处理器到日志器logger.addHandler(ls) 重点笔记BASE_PATH使用Path(__file__).resolve().parent获取保证无论从哪里运行脚本都能正确找到根目录。TimedRotatingFileHandler能自动按时间分割日志避免单个日志文件过大。if logger.handlers:判断防止多次调用配置函数导致日志重复打印。2.3base_page.py—— 基础页面类封装通用操作importloggingfromselenium.webdriver.support.waitimportWebDriverWaitfromutilsimportDriverUtils# 导入日志、显式等待、我们自己写的驱动工具classBuyerBasePage:# 买家端页面基类def__init__(self):self.driverDriverUtils.get_buyer_driver()# 获取买家端驱动deffind_el(self,location):# 显示等待定位单个元素try:elWebDriverWait(self.driver,10,1).until(lambdax:x.find_element(*location)# 将元组解包传递给find_element)logging.info(fexecute find_el{location}success!)exceptExceptionase:logging.error(fexecute find_el{location}failed!)elNone# 定位失败返回Nonereturneldefinput_text(self,el,key_text):# 清空输入框并输入文本try:el.clear()# 清空原有内容logging.info(fexcute clear{el}s text success!)el.send_keys(key_text)# 输入新内容logging.info(fexcute input{el}s{key_text}success!)exceptExceptionase:logging.error(fexcute input{el}s{key_text}failed!)defswitch_window(self,n):# 切换到第n个窗口handlesself.driver.window_handles# 获取所有窗口句柄self.driver.switch_to.window(handles[n])defswitch_frame(self,el):# 切换到指定的iframeself.driver.switch_to.frame(el)classAdminBasePage:# 后台管理系统页面基类结构与买家端相同仅驱动不同def__init__(self):self.driverDriverUtils.get_admin_driver()# 获取后台驱动deffind_el(self,location):try:elWebDriverWait(self.driver,10,1).until(lambdax:x.find_element(*location))logging.info(fexecute find_el{location}success!)exceptExceptionase:logging.error(fexecute find_el{location}failed!)elNonereturneldefinput_text(self,el,key_text):try:el.clear()logging.info(fexcute clear{el}s text success!)el.send_keys(key_text)logging.info(fexcute input{el}s{key_text}success!)exceptExceptionase:logging.error(fexcute input{el}s{key_text}failed!)defswitch_window(self,n):handlesself.driver.window_handles self.driver.switch_to.window(handles[n])defswitch_frame(self,el):self.driver.switch_to.frame(el)classAppBasePage:# APP端页面基类预留def__init__(self):self.driverDriverUtils.get_app_driver()# 获取APP驱动若未实现会抛异常# 其他方法与上面类似此处略... 重点笔记基类把常用的find_element、send_keys等二次封装增加了显式等待和日志记录提高稳定性和可维护性。元素定位符用元组(By.XXX, value)的形式传递通过*location解包给find_element。2.4 页面对象层Page Object登录页面login_page.pyimporttimefrombase.base_pageimportAdminBasePagefromselenium.webdriver.common.byimportBy# 导入时间模块、后台基类、Selenium定位方式classAdminLoginPage(AdminBasePage):def__init__(self):super().__init__()# 调用父类构造获取driver# 定义页面元素的定位方式元组self.username(By.NAME,username)# 用户名输入框self.password(By.NAME,password)# 密码输入框self.code(By.NAME,vertify)# 验证码输入框self.login_btn(By.NAME,submit)# 登录按钮defadmin_login(self,username,password,code):# 执行登录操作# 输入用户名self.input_text(elself.find_el(self.username),key_textusername)# 输入密码self.input_text(elself.find_el(self.password),key_textpassword)# 输入验证码self.input_text(elself.find_el(self.code),key_textcode)time.sleep(3)# 等待验证码识别或手动输入实际项目中可优化# 点击登录按钮self.find_el(self.login_btn).click()这里如果觉得元素定位一直出错可使用edge浏览器的插件点击你要定位的元素右键即可后台主页admin_home.pyfromselenium.webdriver.common.byimportByfrombase.base_pageimportAdminBasePage# 导入定位方式和后台基类classHomePage(AdminBasePage):def__init__(self):super().__init__()self.goods_shop_link(By.XPATH,//li[data-paramgoods]//img)# 一级菜单商品self.goods_list(By.XPATH,//li[classactive]//a[contains(text(),商品列表)])# 二级菜单defto_goods_page(self):# 点击菜单进入商品列表页self.find_el(self.goods_shop_link).click()# 点击一级菜单self.find_el(self.goods_list).click()# 点击二级菜单商品管理页面admin_goods.pyfromselenium.webdriver.common.byimportByfromselenium.webdriver.support.uiimportSelect# 下拉框操作类frombase.base_pageimportAdminBasePage# 导入定位方式、Select类、后台基类classGoodsPage(AdminBasePage):def__init__(self):super().__init__()self.iframe(By.ID,workspace)# 主内容区所在的iframeself.add_goods_btn(By.XPATH,//i[classfa fa-plus])# 添加商品按钮self.goods_name(By.NAME,goods_name)# 商品名称输入框self.one_catgory(By.ID,cat_id)# 一级分类下拉self.two_catgory(By.ID,cat_id_2)# 二级分类下拉self.three_catgory(By.ID,cat_id_3)# 三级分类下拉self.shop_price(By.NAME,shop_price)# 本店售价self.market_price(By.NAME,market_price)# 市场价格self.is_post(By.ID,is_free_shipping_label_1)# 是否包邮单选框self.submit_btn(By.ID,submit)# 提交按钮defadd_goods(self,g_name,oc,tc,thc,s_price,m_price):# 新增商品操作# 1. 先切换到商品表单所在的iframe否则无法定位内部元素self.driver.switch_to.frame(self.find_el(self.iframe))# 2. 点击“添加商品”按钮self.find_el(self.add_goods_btn).click()# 3. 输入商品名称注意此处是变量g_name而不是字符串g_nameself.input_text(self.find_el(self.goods_name),g_name)# 4. 通过Select类操作三个下拉分类按value值选择Select(self.find_el(self.one_catgory)).select_by_value(oc)Select(self.find_el(self.two_catgory)).select_by_value(tc)Select(self.find_el(self.three_catgory)).select_by_value(thc)# 5. 输入售价和市场价格self.input_text(self.find_el(self.shop_price),s_price)self.input_text(self.find_el(self.market_price),m_price)# 6. 点击“包邮”单选框self.find_el(self.is_post).click()# 7. 点击提交按钮self.find_el(self.submit_btn).click()# 注意操作完毕后可切回默认主文档此处根据情况可加本项目未加 重点笔记Select是 Selenium 专门处理select下拉框的类支持按 value、index、visible_text 选择。如果页面使用了iframe必须先switch_to.frame()才能操作里面的元素否则会报NoSuchElementException。提交商品后通常会有成功提示这部分断言在测试用例中完成。2.5 测试用例 —— 登录并新增商品test_goods.pyimporttimeimportddddocr# 用于识别简单验证码fromselenium.webdriver.common.byimportByfromselenium.webdriver.support.waitimportWebDriverWaitfromselenium.webdriver.supportimportexpected_conditionsasEC# 导入时间、OCR库、定位方式、显式等待、预期条件fromscript.admin.login_pageimportAdminLoginPagefromscript.admin.admin_homeimportHomePagefromscript.admin.admin_goodsimportGoodsPagefromutilsimportDriverUtils# 导入自己写的页面类和驱动工具classTestAdminLogin:defsetup_class(self):# 整个测试类开始前执行一次打开浏览器并访问后台self.driverDriverUtils.get_admin_driver()# 获取后台驱动已内置地址defteardown_class(self):# 整个测试类结束后执行一次关闭浏览器DriverUtils.quit_admin_driver()deftest_add_goods(self):# 测试用例登录后台新增一个商品# 1. 等待验证码图片加载出来captcha_elWebDriverWait(self.driver,10).until(EC.visibility_of_element_located((By.ID,imgVerify)))# 2. 用 ddddocr 识别验证码ocrddddocr.DdddOcr()codeocr.classification(captcha_el.screenshot_as_png)# 截图并识别print(f识别到的验证码:{code})# 3. 执行登录AdminLoginPage().admin_login(admin,123456,code)time.sleep(3)# 等待登录跳转# 4. 通过主页进入商品管理HomePage().to_goods_page()time.sleep(3)# 等待页面加载# 5. 准备商品数据并执行新增goods_namefgoods_{time.strftime(%Y%m%d%H%M%S)}# 生成唯一商品名GoodsPage().add_goods(goods_name,12,13,15,210,220)# 6. 断言可以增加检查页面是否出现“添加成功”等提示本例暂略time.sleep(3)# 实际项目中应替换为显式等待断言 重点笔记ddddocr是一个开源的验证码识别库适合简单的数字/字母验证码复杂验证码仍需开提供绕过方式。商品名加了时间戳避免重复后台可能要求商品名唯一。最后的time.sleep是临时方案实际应用建议用显式等待某个成功提示的元素。三、新手最容易踩的坑元素定位错误务必确认元素是否在iframe中、是否因为动态加载还没出现。多用显式等待。变量与字符串混淆如input_text(..., g_name)必须传变量不能写成g_name。驱动未关闭每次测试结束记得调用quit_xxx_driver()否则浏览器进程会残留。路径问题使用os.path.join(BASE_PATH, ...)构建路径不要硬编码斜杠。验证码处理OCR 不是100%准确最好在测试环境设置固定验证码或让开发提供万能码。四、总结通过这个项目你学会了使用 Selenium 的显式等待、iframe 切换、下拉框操作如何用 Page Object 模式组织代码如何封装驱动管理、日志配置、数据读取等公共模块如何用ddddocr解决简单验证码问题。这套框架可以直接复用到任何 Web 自动化项目中只需根据实际页面修改元素定位和业务逻辑即可。希望这篇博客能成为你自动化学习路上的有力助手如果本文对你有帮助欢迎点赞、收藏、转发让更多小伙伴一起学习进步如有任何问题欢迎在评论区交流讨论。