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_elementsend_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 是临时方案,实际应用建议用显式等待某个成功提示的元素。

三、新手最容易踩的坑

  1. 元素定位错误:务必确认元素是否在 iframe 中、是否因为动态加载还没出现。多用显式等待。
  2. 变量与字符串混淆:如 input_text(..., g_name) 必须传变量,不能写成 "g_name"
  3. 驱动未关闭:每次测试结束记得调用 quit_xxx_driver(),否则浏览器进程会残留。
  4. 路径问题:使用 os.path.join(BASE_PATH, ...) 构建路径,不要硬编码斜杠。
  5. 验证码处理:OCR 不是100%准确,最好在测试环境设置固定验证码,或让开发提供万能码。

四、总结

通过这个项目,你学会了:

  • 使用 Selenium 的显式等待、iframe 切换、下拉框操作;
  • 如何用 Page Object 模式组织代码;
  • 如何封装驱动管理、日志配置、数据读取等公共模块;
  • 如何用 ddddocr 解决简单验证码问题。

这套框架可以直接复用到任何 Web 自动化项目中,只需根据实际页面修改元素定位和业务逻辑即可。希望这篇博客能成为你自动化学习路上的有力助手!


如果本文对你有帮助,欢迎点赞、收藏、转发,让更多小伙伴一起学习进步!如有任何问题,欢迎在评论区交流讨论。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐