Python Selenium自动化测试框架搭建实战-商城
·
Python Selenium 自动化测试框架搭建与实战(TPshop 商城为例)
本文面向初学者,通过一个完整的 TPshop 商城后台自动化测试项目,拆解如何搭建 Selenium 自动化框架。所有代码注释均在代码下方,帮你彻底理解每一句代码的作用。本文仅供学习笔记使用,不涉及商业用途。
📑 目录
一、项目结构总览
project/
├── base/
│ └── base_page.py # 基础页面类(封装通用操作)
├── config.py # 全局配置:路径、日志、数据读取
├── utils.py # 驱动管理、自定义工具函数
├── script/
│ └── admin/
│ ├── login_page.py # 登录页面对象
│ ├── admin_home.py # 后台主页对象
│ └── admin_goods.py # 商品管理页面对象
└── test_goods.py # 测试用例:登录并新增商品

二、核心模块逐行详解
2.1 utils.py —— 驱动工厂与通用等待
import logging
import re
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
# 导入日志模块,正则模块,Selenium的显式等待和定位方式
class DriverUtils:
# 定义三个私有类变量,分别保存买家、后台、APP的驱动对象,初始为空
__buyer_driver = None # 买家端浏览器驱动
__admin_driver = None # 后台管理系统浏览器驱动
__app_driver = None # APP端驱动(预留)
@classmethod
def get_buyer_driver(cls):
# 获取买家端的浏览器驱动,如果还没有就创建一个新的
if cls.__buyer_driver is None: # 如果驱动对象不存在
from selenium import webdriver # 动态导入webdriver
cls.__buyer_driver = webdriver.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")
return cls.__buyer_driver # 返回驱动对象
@classmethod
def quit_buyer_driver(cls):
# 关闭并清理买家端驱动
if cls.__buyer_driver is not None: # 如果驱动对象存在
cls.__buyer_driver.quit() # 关闭浏览器
cls.__buyer_driver = None # 将变量重置为空
@classmethod
def get_admin_driver(cls):
# 获取后台管理系统的浏览器驱动
if cls.__admin_driver is None:
from selenium import webdriver
cls.__admin_driver = webdriver.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")
return cls.__admin_driver
@classmethod
def quit_admin_driver(cls):
# 关闭并清理后台驱动
if cls.__admin_driver is not None:
cls.__admin_driver.quit()
cls.__admin_driver = None
@classmethod
def get_app_driver(cls):
# 获取APP驱动(目前尚未实现,调用会报错提醒)
if cls.__app_driver is None:
raise NotImplementedError("APP驱动尚未实现") # 抛出未实现异常
return cls.__app_driver
@classmethod
def quit_app_driver(cls):
# 关闭APP驱动
if cls.__app_driver is not None:
cls.__app_driver.quit()
cls.__app_driver = None
def el_is_exist_by_text(driver, key_text):
# 根据页面文本判断元素是否存在,存在返回元素对象,不存在返回False并截图
safe_text = key_text.replace('"', '\\"') # 转义双引号,防止XPath语法错误
try:
# 显式等待直到找到包含指定文本的元素
is_suc = WebDriverWait(driver, 10, 1).until(
lambda x: x.find_element(By.XPATH, f'//*[text()="{safe_text}"]')
)
except Exception as e:
is_suc = False # 没找到则返回False
# 清理文件名中的非法字符(例如 / \ : * ? " < > |)
safe_name = re.sub(r'[\\/*?:"<>|]', "_", key_text)
driver.get_screenshot_as_file(f"{safe_name}_未找到.png") # 失败截图
logging.error(f"未找到文本为{key_text}的元素对象!")
return is_suc # 返回查找结果
def get_el_text(driver, xpath_str):
# 根据xpath获取元素的文本内容
msg = None
try:
# 显式等待定位元素并获取其text属性
msg = WebDriverWait(driver, 10, 1).until(
lambda x: x.find_element(By.XPATH, xpath_str)
).text
logging.info(msg) # 用日志输出文本内容
except Exception as e:
logging.error(f"没有获取到{xpath_str}的元素对象文本!")
msg = None
return msg
📌 重点笔记:
- 使用类属性
__buyer_driver实现单例模式,整个测试过程只有一个浏览器窗口,节省资源。 implicitly_wait(5)是隐式等待,全局生效;WebDriverWait是显式等待,仅对当次查找生效。- 截图文件名要避免特殊字符,否则会导致保存失败。
2.2 config.py —— 全局配置与日志
import os
import json
import logging
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path
# 导入系统路径、JSON、日志、日志切割处理器、现代路径库
# 获取当前文件所在目录,作为项目根目录
BASE_PATH = Path(__file__).resolve().parent
def build_data(file_name):
# 读取JSON测试数据,返回二维列表供参数化使用
filepath = os.path.join(BASE_PATH, "data", f"{file_name}.json") # 拼接完整路径
case_data = [] # 存放所有用例数据
try:
with open(filepath, encoding="utf-8") as f:
all_data = json.load(f) # 读取JSON转为字典
except (FileNotFoundError, json.JSONDecodeError) as e:
logging.error(f"读取数据文件失败: {filepath} - {e}")
raise # 重新抛出异常
for i in all_data.values(): # 遍历字典的每个值(也是字典)
case_data.append(list(i.values())) # 提取值转为列表,加入结果
return case_data
def basic_log_config():
# 配置日志:同时输出到文件和控制台
logger = logging.getLogger() # 获取根日志器
if logger.handlers: # 防止重复添加处理器
return
logger.setLevel(logging.INFO) # 设置全局日志级别
# 创建一个按天切割的日志文件处理器,保留2个备份
lht = TimedRotatingFileHandler(
filename=os.path.join(BASE_PATH, "log", "tp_test.log"), # 日志文件路径
when='midnight', # 每天午夜切割
interval=1, # 间隔1天
backupCount=2, # 只保留最近2个备份
encoding="utf8"
)
ls = logging.StreamHandler() # 输出到控制台
formatter = logging.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.3 base_page.py —— 基础页面类(封装通用操作)
import logging
from selenium.webdriver.support.wait import WebDriverWait
from utils import DriverUtils
# 导入日志、显式等待、我们自己写的驱动工具
class BuyerBasePage:
# 买家端页面基类
def __init__(self):
self.driver = DriverUtils.get_buyer_driver() # 获取买家端驱动
def find_el(self, location):
# 显示等待定位单个元素
try:
el = WebDriverWait(self.driver, 10, 1).until(
lambda x: x.find_element(*location) # 将元组解包传递给find_element
)
logging.info(f"execute find_el {location} success!")
except Exception as e:
logging.error(f"execute find_el {location} failed!")
el = None # 定位失败返回None
return el
def input_text(self, el, key_text):
# 清空输入框并输入文本
try:
el.clear() # 清空原有内容
logging.info(f"excute clear {el}'s text success!")
el.send_keys(key_text) # 输入新内容
logging.info(f"excute input {el}'s {key_text} success!")
except Exception as e:
logging.error(f"excute input {el}'s {key_text} failed!")
def switch_window(self, n):
# 切换到第n个窗口
handles = self.driver.window_handles # 获取所有窗口句柄
self.driver.switch_to.window(handles[n])
def switch_frame(self, el):
# 切换到指定的iframe
self.driver.switch_to.frame(el)
class AdminBasePage:
# 后台管理系统页面基类(结构与买家端相同,仅驱动不同)
def __init__(self):
self.driver = DriverUtils.get_admin_driver() # 获取后台驱动
def find_el(self, location):
try:
el = WebDriverWait(self.driver, 10, 1).until(
lambda x: x.find_element(*location)
)
logging.info(f"execute find_el {location} success!")
except Exception as e:
logging.error(f"execute find_el {location} failed!")
el = None
return el
def input_text(self, el, key_text):
try:
el.clear()
logging.info(f"excute clear {el}'s text success!")
el.send_keys(key_text)
logging.info(f"excute input {el}'s {key_text} success!")
except Exception as e:
logging.error(f"excute input {el}'s {key_text} failed!")
def switch_window(self, n):
handles = self.driver.window_handles
self.driver.switch_to.window(handles[n])
def switch_frame(self, el):
self.driver.switch_to.frame(el)
class AppBasePage:
# APP端页面基类(预留)
def __init__(self):
self.driver = DriverUtils.get_app_driver() # 获取APP驱动,若未实现会抛异常
# 其他方法与上面类似,此处略...
📌 重点笔记:
- 基类把常用的
find_element、send_keys等二次封装,增加了显式等待和日志记录,提高稳定性和可维护性。 - 元素定位符用元组
(By.XXX, "value")的形式传递,通过*location解包给find_element。
2.4 页面对象层(Page Object)
登录页面 login_page.py
import time
from base.base_page import AdminBasePage
from selenium.webdriver.common.by import By
# 导入时间模块、后台基类、Selenium定位方式
class AdminLoginPage(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") # 登录按钮
def admin_login(self, username, password, code):
# 执行登录操作
# 输入用户名
self.input_text(el=self.find_el(self.username), key_text=username)
# 输入密码
self.input_text(el=self.find_el(self.password), key_text=password)
# 输入验证码
self.input_text(el=self.find_el(self.code), key_text=code)
time.sleep(3) # 等待验证码识别或手动输入(实际项目中可优化)
# 点击登录按钮
self.find_el(self.login_btn).click()


这里如果觉得元素定位一直出错可使用,edge浏览器的插件,点击你要定位的元素右键即可
后台主页 admin_home.py
from selenium.webdriver.common.by import By
from base.base_page import AdminBasePage
# 导入定位方式和后台基类
class HomePage(AdminBasePage):
def __init__(self):
super().__init__()
self.goods_shop_link = (By.XPATH, "//li[@data-param='goods']//img") # 一级菜单:商品
self.goods_list = (By.XPATH, "//li[@class='active']//a[contains(text(),'商品列表')]") # 二级菜单
def to_goods_page(self):
# 点击菜单进入商品列表页
self.find_el(self.goods_shop_link).click() # 点击一级菜单
self.find_el(self.goods_list).click() # 点击二级菜单
商品管理页面 admin_goods.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select # 下拉框操作类
from base.base_page import AdminBasePage
# 导入定位方式、Select类、后台基类
class GoodsPage(AdminBasePage):
def __init__(self):
super().__init__()
self.iframe = (By.ID, "workspace") # 主内容区所在的iframe
self.add_goods_btn = (By.XPATH, "//i[@class='fa 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") # 提交按钮
def add_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_name")
self.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.py
import time
import ddddocr # 用于识别简单验证码
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 导入时间、OCR库、定位方式、显式等待、预期条件
from script.admin.login_page import AdminLoginPage
from script.admin.admin_home import HomePage
from script.admin.admin_goods import GoodsPage
from utils import DriverUtils
# 导入自己写的页面类和驱动工具
class TestAdminLogin:
def setup_class(self):
# 整个测试类开始前执行一次:打开浏览器并访问后台
self.driver = DriverUtils.get_admin_driver() # 获取后台驱动(已内置地址)
def teardown_class(self):
# 整个测试类结束后执行一次:关闭浏览器
DriverUtils.quit_admin_driver()
def test_add_goods(self):
# 测试用例:登录后台,新增一个商品
# 1. 等待验证码图片加载出来
captcha_el = WebDriverWait(self.driver, 10).until(
EC.visibility_of_element_located((By.ID, "imgVerify"))
)
# 2. 用 ddddocr 识别验证码
ocr = ddddocr.DdddOcr()
code = ocr.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_name = f"goods_{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 自动化项目中,只需根据实际页面修改元素定位和业务逻辑即可。希望这篇博客能成为你自动化学习路上的有力助手!
如果本文对你有帮助,欢迎点赞、收藏、转发,让更多小伙伴一起学习进步!如有任何问题,欢迎在评论区交流讨论。
更多推荐


所有评论(0)