Selenium+Pytest自动化测试框架实战

🔥 从零搭建 Selenium + Pytest 自动化测试框架(PO 模式实战)

作者:[你的名字]

发布日期:2026-07-28

关键词:Selenium、Pytest、Page Object、UI 自动化、驱动管理、日志配置


📌 一、写在前面

在 UI 自动化测试中,代码的可维护性稳定性 是两大核心难题。本文将带你完整实现一个"商品搜索 → 加入购物车 → 断言成功"的自动化用例,并贯穿 Page Object(PO)设计模式显式等待单例驱动管理日志与 Allure 报告等实用技术。

全程基于你实际项目的真实代码,所有代码均保留原貌,仅增加注释,可直接复制运行


📁 二、项目目录结构

text 复制代码
项目根目录/
├── config.py                # 全局配置(路径、日志、数据读取)
├── utils.py                 # 工具类(驱动管理、元素文本获取)
├── base/
│   └── base_page.py         # (需自行补充)基础页面父类,封装 find_el 等
├── page/
│   └── buyer_page/
│       ├── index_page.py    # 首页页面对象(搜索)
│       └── goods_page.py    # (需自行补充)商品详情页页面对象
├── script/
│   └── buyer_script/
│       └── test_cart.py     # 测试用例(添加购物车)
├── data/                    # 测试数据(JSON)
└── log/                     # 日志输出目录

⚠️ 注:base_page.pygoods_page.py 是依赖文件,我会在后文给出接口说明,你可以根据实际项目实现。


🔧 三、基础组件详解(核心代码 + 注释)

1. 全局配置 config.py ------ 统一管理项目路径、日志、数据驱动

知识点

  • Path(__file__).resolve().parent 获取项目根目录,避免硬编码。
  • TimedRotatingFileHandler 实现日志按天切割,便于归档。
  • build_data 函数支持从 JSON 文件读取测试数据,为数据驱动测试(DDT)铺路。
python 复制代码
import os
import json
import logging
from logging.handlers import TimedRotatingFileHandler
from pathlib import Path

# 获取项目根目录的绝对路径(无论脚本在哪里执行,都能正确定位)
BASE_PATH = Path(__file__).resolve().parent

def build_data(file_name):
    """
    从 data/ 目录读取指定 JSON 文件,返回列表格式的测试数据。
    用于 pytest 的 @pytest.mark.parametrize 参数化。
    """
    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():
    """
    配置日志:同时输出到控制台和文件(每天午夜切割,保留 2 个备份)。
    避免重复添加 Handler(通过判断 logger.handlers 是否为空)。
    """
    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,
        backupCount=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)

2. 工具类 utils.py ------ 驱动管理 + 显式等待封装

知识点

  • 单例模式:通过类变量确保每个端口(买家/后台/APP)只有一个驱动实例。
  • 后台驱动开关设计__admin_key 控制是否真正关闭浏览器,解决「多用例共用浏览器会话」的痛点(避免登录态丢失)。
  • get_el_text :封装显式等待获取元素文本,失败返回 None 并记录日志,便于断言调试。
python 复制代码
import logging
import re
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By

class DriverUtils:
    """
    驱动管理工具类(类变量实现单例)
    """
    # 三个端口的驱动变量(全局唯一)
    __buyer_driver = None
    __admin_driver = None
    __app_driver = None

    # 后台驱动关闭开关(默认 False,防止误关浏览器)
    __admin_key = False

    # ---------- 买家端 ----------
    @classmethod
    def get_buyer_driver(cls):
        """获取买家端驱动(若为空则新建 Edge 实例,并自动打开首页)"""
        if cls.__buyer_driver is None:
            from selenium import webdriver
            cls.__buyer_driver = webdriver.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):
        """获取后台驱动(仅创建,不自动访问任何 URL,由测试自行 navigate)"""
        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)
        return cls.__admin_driver

    @classmethod
    def change_admin_key(cls, key):
        """外部修改后台驱动关闭开关(True 允许关闭,False 禁止关闭)"""
        cls.__admin_key = key
        logging.info(f"后台驱动开关已修改为: {key}")

    @classmethod
    def quit_admin_driver(cls):
        """
        安全关闭后台驱动:
        - 若开关为 True,则真正执行 driver.quit() 关闭浏览器窗口。
        - 若开关为 False,仅将类变量置为 None,不关闭窗口(保留会话)。
        - 无论哪种情况,都将变量置空,确保下次 get 时重建新驱动。
        - 最后将开关重置为 False,避免影响其他操作。
        """
        if cls.__admin_driver is not None:
            if cls.__admin_key:
                cls.__admin_driver.quit()
                logging.info("后台浏览器已成功关闭")
            else:
                logging.warning("后台驱动开关为 False,本次未实际关闭浏览器,仅清理对象引用")

            cls.__admin_driver = None
            cls.__admin_key = False  # 使用后复位

    # ---------- APP端(预留) ----------
    @classmethod
    def get_app_driver(cls):
        if cls.__app_driver is None:
            raise NotImplementedError("APP驱动尚未实现")
        return cls.__app_driver

    @classmethod
    def quit_app_driver(cls):
        if cls.__app_driver is not None:
            cls.__app_driver.quit()
            cls.__app_driver = None

# ---------- 通用页面工具函数 ----------
def el_is_exist_by_text(driver, key_text):
    """
    通过文本内容判断元素是否存在(显式等待,最长 10 秒)。
    若存在返回 WebElement 对象(真值),若超时返回 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
        # 将文本中的非法文件名字符替换为下划线,防止截图保存失败
        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 定位元素并返回其文本内容。
    成功返回文本(字符串),失败返回 None 并记录错误日志。
    """
    msg = None
    try:
        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

3. 首页页面对象 page/buyer_page/index_page.py ------ 封装搜索业务

知识点

  • PO 模式:将页面元素定位与业务操作分离,提高复用性。
  • 继承 BuyerBasePage(需自行实现),使用父类的 find_elinput_text 方法。
  • 定义定位器为元组 (By.ID, "q"),便于统一管理。
python 复制代码
from selenium.webdriver.common.by import By
from base.base_page import BuyerBasePage

class IndexPage(BuyerBasePage):
    """
    买家首页页面对象
    """
    def __init__(self, driver):
        self.driver = driver
        super().__init__()

        # 定位器:搜索输入框(ID 定位)
        self.search_box = (By.ID, "q")
        # 定位器:搜索按钮(XPath 定位,匹配 type='submit' 的 button)
        self.search_btn = (By.XPATH, "//button[@type='submit']")

    def query_goods(self, key_word):
        """
        业务方法:在首页搜索商品
        1. 在搜索框输入关键词
        2. 点击搜索按钮
        """
        # 调用父类的 input_text 方法(先清空再输入)
        self.input_text(self.find_el(self.search_box), key_word)
        # 找到搜索按钮并点击
        self.find_el(self.search_btn).click()

4. 测试用例 script/buyer_script/test_cart.py ------ 完整场景 + 断言

知识点

  • 使用 setup_class / teardown_class 管理浏览器生命周期(整个类只启动/关闭一次)。
  • 调用页面对象完成业务操作。
  • 断言使用 get_el_text 获取提示信息,并通过 assert 验证。
  • 异常处理中嵌入 Allure 截图,方便失败时快速定位问题。
python 复制代码
# 定义测试类
from config import BASE_PATH
from page.buyer_page.goods_page import GoodsPage
from utils import DriverUtils, get_el_text
import allure
from utils import DriverUtils,el_is_exist_by_text
from page.buyer_page.index_page import IndexPage
import time

class TestGoods:
    def setup_class(self):
        # 打开浏览器(获取买家驱动)
        self.driver = DriverUtils.get_buyer_driver()
        # 直接打开搜索页(带关键词,也可通过首页搜索)
        self.driver.get("http://192.168.1.161/Home/Goods/search.html?q=goods_20260726191738")

    def teardown_class(self):
        # 关闭浏览器
        DriverUtils.quit_buyer_driver()

    # 定义测试方法
    def test_add_cart(self):
        # 1. 在首页搜索框输入商品关键词并搜索
        IndexPage(self.driver).query_goods("goods_20260726191738")
        # 2. 进入商品详情页并点击加入购物车(由 GoodsPage 封装)
        GoodsPage(self.driver).add_goods_cart()
        # 3. 断言:判断是否弹出"添加成功"提示
        try:
            # 调用 get_el_text 获取提示框文本(注意 class 名是 conect-title 而非 content-title)
            msg = get_el_text(self.driver,"//*[@class='conect-title']/span")
            assert "添加成功" in msg
        except Exception as e:
            # 若断言失败,则截图并附加到 Allure 报告中
            allure.attach(self.driver.get_screenshot_as_png(),
                          BASE_PATH + "\img\test_add_cart.png",
                          allure.attachment_type.PNG)
            raise e
        time.sleep(2)   # 等待页面渲染,便于观察结果

🧪 四、补充软件测试核心知识(扩展阅读)

1. 为什么用 Page Object(PO)模式?

  • 提高可维护性:页面元素变化只需修改对应 Page 类,测试脚本无需改动。
  • 增强复用性 :多个测试用例可共享同一个 Page 类的业务方法(如 query_goods)。
  • 清晰分层:测试层(用例)只关心业务逻辑,不关心底层 Selenium 操作。

2. 显式等待 vs 隐式等待

类型 作用范围 特点
隐式等待 (implicitly_wait) 全局,作用于整个 WebDriver 生命周期 设置超时时间,轮询查找元素,直到找到或超时。
显式等待 (WebDriverWait) 局部,针对特定元素 更灵活,可设置轮询间隔、忽略异常,支持自定义条件。

最佳实践:混合使用,隐式等待作为全局保底,显式等待针对关键元素(如弹窗、异步加载内容)。

3. 驱动单例与后台会话保持

  • 使用类变量实现单例,避免每个测试都创建新浏览器,节省资源。
  • 后台管理测试中,通常需要保持登录状态跨多个用例。通过 __admin_key 开关控制关闭时机,既能清理资源,又能复用浏览器窗口,极大提升执行效率。

4. 日志与 Allure 报告

  • 日志:记录运行关键信息,便于定位环境问题(如元素未找到、超时)。
  • Allure:提供美观的测试报告,支持截图、步骤描述、参数展示,是团队协作和问题复现的利器。

5. 数据驱动测试(DDT)

  • 将测试数据从脚本中分离,存放在 JSON / Excel / YAML 文件中。
  • build_data 函数读取 JSON 文件,配合 @pytest.mark.parametrize 实现一个测试方法跑多组数据,极大减少重复代码。

🚀 五、运行与调试

环境准备

  • Python 3.11+
  • 安装依赖:pip install selenium pytest allure-pytest
  • 下载 Edge 浏览器驱动(或 Chrome)并配置到 PATH。

执行命令

bash 复制代码
# 运行单个测试文件
pytest script/buyer_script/test_cart.py -v

# 生成 Allure 报告(需提前安装 allure 命令行)
pytest script/buyer_script/test_cart.py --alluredir=report
allure serve report

⚠️ 注意:运行前请确保 base_page.pygoods_page.py 已实现(或根据你的项目调整导入),并创建 log/data/ 目录(若未创建,日志写入会报错)。


📊 六、常见踩坑与解决方案(来自真实调试记录)

现象 原因 解决方案
assert "添加成功" in msg 报 TypeError(msg 为 None) XPath 写错,实际 class 名为 conect-title 而非 content-title 使用浏览器 F12 → 右键元素 → Copy XPath,杜绝手写错误
BASE_PATH + "\img\test_add_cart.png" 报类型错误 BASE_PATHPath 对象,不能直接加字符串 使用 os.path.join(BASE_PATH, "img", "test_add_cart.png")str(BASE_PATH)
多个用例执行时,第二个用例报 InvalidSessionIdException 上一个用例关闭了浏览器,驱动会话失效 使用 DriverUtils 的后台开关,保持浏览器常驻

✅ 七、总结

本文从零开始,构建了一个企业级 UI 自动化框架的雏形,核心亮点包括:

  • 清晰的目录分层与 PO 封装
  • 健壮的驱动管理(支持多端 + 后台会话保持)
  • 日志与 Allure 报告集成
  • 数据驱动预留接口

所有代码均来自真实项目,可直接运行。希望这份笔记能帮助你快速上手自动化测试,提升测试效率与代码质量。

📎 如果你需要完整的 base_page.pygoods_page.py 实现,欢迎在评论区留言,我会尽快补充。


如果觉得有用,请点赞、收藏、分享,让更多测试小伙伴受益! 🎉

相关推荐
CYB353219 小时前
KEYSIGHT是德科技 N5173B 微波模拟信号发生器
功能测试·科技·测试工具
软件资深者1 天前
QQ音乐PC版 使用教程:去广告绿色便携版音乐播放器,本地曲库管理与在线试听一键搞定,音乐播放器新手 5 分钟上手(2026)
测试工具·容器·娱乐·软件需求
CYB35321 天前
KEYSIGHT N9330B 是德科技手持式电缆和天线测试仪
功能测试·科技·测试工具
祉猷并茂,雯华若锦2 天前
Pytest+Selenium自动化测试:失败自动截图集成Allure报告
selenium·测试工具·pytest
黑客-秋凌3 天前
使用Python+selenium实现第一个自动化测试脚本
开发语言·自动化测试·软件测试·python·selenium·测试工具
啦啦啦!3 天前
基于AI进行GUI自动化测试
功能测试·测试工具·ai编程
祉猷并茂,雯华若锦4 天前
Win下完美解决Allure报错,生成Web自动化测试报告
android·python·selenium·自动化
gongzhxu4 天前
DumpAny —— 一款支持 HTTP / gRPC / MySQL / Redis 的全能网络调试工具
网络·测试工具·http·rpc
思-无-涯4 天前
测试面试新趋势:工程思维成关键
人工智能·功能测试·测试工具·职场和发展·可用性测试