Python UI 自动化测试框架搭建demo(Selenium+Pytest 版)

本次搭建的是轻量可扩展的 UI 自动化测试框架 ,基于Selenium (UI 操作)+Pytest (测试执行 / 用例管理),整合PO 设计模式 (页面对象)、配置文件管理日志记录截图报告数据驱动核心能力,适合 Web 端 UI 自动化入门,可直接运行并扩展。

一、框架整体结构

采用分层解耦设计,便于维护和扩展,目录结构清晰,各模块职责单一,完整结构如下:

bash 复制代码
ui_auto_framework/
├── config/                # 配置文件目录
│   └── config.ini         # 全局配置(浏览器、网址、超时时间等)
├── data/                  # 测试数据目录
│   └── test_data.yaml     # 数据驱动的测试数据(yaml格式)
├── logs/                  # 日志输出目录(自动生成)
├── page/                  # PO模式-页面对象层
│   ├── base_page.py       # 基础页面类(封装通用操作)
│   └── baidu_page.py      # 业务页面类(示例:百度首页)
├── report/                # 测试报告/截图目录(自动生成)
├── test_case/             # 测试用例层
│   └── test_baidu.py      # 百度搜索测试用例(示例)
├── utils/                 # 工具类目录
│   ├── log_util.py        # 日志工具
│   ├── config_util.py     # 配置读取工具
│   ├── screenshot_util.py # 截图工具
│   └── driver_util.py     # 浏览器驱动工具
├── conftest.py            # Pytest全局夹具(驱动初始化/销毁)
├── pytest.ini             # Pytest全局配置
├── requirements.txt       # 项目依赖包
└── run.py                 # 框架运行入口(一键执行用例+生成报告)
  1. 安装依赖包

创建requirements.txt,写入以下依赖,执行pip install -r requirements.txt安装:

bash 复制代码
# 核心UI自动化
selenium==4.15.2
# 测试框架
pytest==7.4.3
pytest-html==4.0.2  # Pytest HTML测试报告
pytest-rerunfailures==12.0  # 失败重跑
# 配置/数据解析
pyyaml==6.0.1
configparser==5.3.0
# 日志/时间
logging
datetime
# 浏览器驱动管理(自动适配浏览器版本)
webdriver-manager==4.0.1

2. 浏览器驱动说明

使用webdriver-manager自动管理 Chrome/Firefox/Edge 驱动,无需手动下载配置,框架中已集成,支持一键切换浏览器。

各模块代码实现

一、配置文件模块(config)

1. 全局配置文件config/config.ini

存放浏览器类型、测试网址、超时时间、报告路径等全局配置,通过工具类读取,便于统一修改:

bash 复制代码
[BASE]
# 浏览器类型:chrome/firefox/edge
browser = chrome
# 测试基础网址
base_url = https://www.baidu.com
# 元素定位超时时间(秒)
timeout = 10
# 隐式等待时间(秒)
implicitly_wait = 5

[REPORT]
# 测试报告路径
report_path = ./report/
# 日志路径
log_path = ./logs/

[SCREENSHOT]
# 截图路径(失败用例自动截图)
screenshot_path = ./report/screenshot/
2. 配置读取工具utils/config_util.py

封装configparser,实现配置文件的一键读取,避免重复代码:

python 复制代码
import configparser
import os

# 获取配置文件绝对路径
CONFIG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "config.ini")

class ConfigUtil:
    def __init__(self):
        self.config = configparser.ConfigParser()
        self.config.read(CONFIG_FILE_PATH, encoding="utf-8")

    # 读取字符串配置
    def get_str(self, section, option):
        return self.config.get(section, option)

    # 读取整数配置
    def get_int(self, section, option):
        return self.config.getint(section, option)

# 实例化,供其他模块调用
config = ConfigUtil()

二、工具类模块(utils)

1. 日志工具utils/log_util.py

封装 Python 内置logging,实现日志分级(DEBUG/INFO/WARNING/ERROR)、日志文件按日期生成、控制台 + 文件双输出,便于问题排查:

python 复制代码
import logging
import os
from datetime import datetime
from utils.config_util import config

# 日志目录(从配置文件读取,不存在则创建)
LOG_DIR = config.get_str("REPORT", "log_path")
if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)

# 日志文件命名:日期+log
LOG_FILE = os.path.join(LOG_DIR, f"{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")

class LogUtil:
    def __init__(self, name=__name__):
        # 初始化日志器
        self.logger = logging.getLogger(name)
        self.logger.setLevel(logging.DEBUG)  # 全局日志级别

        # 避免日志重复输出
        if not self.logger.handlers:
            # 格式:时间-日志器-级别-模块-信息
            formatter = logging.Formatter(
                "%(asctime)s - %(name)s - %(levelname)s - %(module)s - %(message)s"
            )

            # 控制台处理器
            console_handler = logging.StreamHandler()
            console_handler.setLevel(logging.INFO)
            console_handler.setFormatter(formatter)

            # 文件处理器
            file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
            file_handler.setLevel(logging.DEBUG)
            file_handler.setFormatter(formatter)

            # 添加处理器
            self.logger.addHandler(console_handler)
            self.logger.addHandler(file_handler)

    def get_logger(self):
        return self.logger

# 实例化,供其他模块调用
logger = LogUtil().get_logger()
2. 浏览器驱动工具utils/driver_util.py

封装浏览器驱动的初始化,支持从配置文件切换浏览器,封装驱动退出,配合 Pytest 夹具使用:

python 复制代码
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.service import Service as FirefoxService
from selenium.webdriver.edge.service import Service as EdgeService
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from webdriver_manager.microsoft import EdgeChromiumDriverManager
from utils.config_util import config
from utils.log_util import logger

class DriverUtil:
    # 初始化驱动
    @staticmethod
    def get_driver():
        browser = config.get_str("BASE", "browser")
        base_url = config.get_str("BASE", "base_url")
        implicitly_wait = config.get_int("BASE", "implicitly_wait")
        driver = None
        try:
            if browser == "chrome":
                driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))
            elif browser == "firefox":
                driver = webdriver.Firefox(service=FirefoxService(GeckoDriverManager().install()))
            elif browser == "edge":
                driver = webdriver.Edge(service=EdgeService(EdgeChromiumDriverManager().install()))
            else:
                logger.error(f"不支持的浏览器:{browser},默认使用Chrome")
                driver = webdriver.Chrome(service=ChromeService(ChromeDriverManager().install()))

            # 浏览器最大化
            driver.maximize_window()
            # 隐式等待
            driver.implicitly_wait(implicitly_wait)
            # 打开基础网址
            driver.get(base_url)
            logger.info(f"成功启动{browser}浏览器,打开网址:{base_url}")
            return driver
        except Exception as e:
            logger.error(f"浏览器启动失败:{str(e)}", exc_info=True)
            raise

    # 退出驱动
    @staticmethod
    def quit_driver(driver):
        if driver:
            driver.quit()
            logger.info("浏览器驱动已退出")

# 实例化(可选,根据需求使用)
driver_util = DriverUtil()
3. 截图工具utils/screenshot_util.py

实现用例失败时自动截图,截图文件按日期 + 用例名命名,保存到配置的截图路径,配合 Pytest 钩子使用:

python 复制代码
import os
from datetime import datetime
from selenium.webdriver.remote.webdriver import WebDriver
from utils.config_util import config
from utils.log_util import logger

# 截图目录(不存在则创建)
SCREENSHOT_DIR = config.get_str("SCREENSHOT", "screenshot_path")
if not os.path.exists(SCREENSHOT_DIR):
    os.makedirs(SCREENSHOT_DIR)

class ScreenshotUtil:
    @staticmethod
    def take_screenshot(driver: WebDriver, case_name: str):
        """
        截取页面截图
        :param driver: 浏览器驱动
        :param case_name: 测试用例名
        :return: 截图文件路径
        """
        try:
            # 截图文件名:日期时间_用例名.png
            screenshot_name = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{case_name}.png"
            screenshot_path = os.path.join(SCREENSHOT_DIR, screenshot_name)
            # 截图并保存
            driver.get_screenshot_as_file(screenshot_path)
            logger.info(f"截图成功,保存路径:{screenshot_path}")
            return screenshot_path
        except Exception as e:
            logger.error(f"截图失败:{str(e)}", exc_info=True)
            raise

# 实例化,供其他模块调用
screenshot_util = ScreenshotUtil()

三、PO 设计模式 - 页面对象层(page)

采用PO(Page Object)设计模式 ,将页面元素和操作封装到页面类中,测试用例仅调用页面方法,实现元素与用例解耦,元素定位修改时仅需改页面类,无需改所有用例。

1. 基础页面类page/base_page.py

封装 Selenium 通用操作(点击、输入、元素定位、等待等),所有业务页面类继承此类,减少重复代码:

python 复制代码
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from utils.config_util import config
from utils.log_util import logger

class BasePage:
    def __init__(self, driver: WebDriver):
        self.driver = driver
        self.timeout = config.get_int("BASE", "timeout")  # 元素定位超时时间

    # 元素定位(显式等待,确保元素可操作)
    def find_element(self, locator):
        """
        定位单个元素
        :param locator: 定位器,元组格式 (By.ID, "kw") / (By.XPATH, "//input")
        :return: 元素对象
        """
        try:
            element = WebDriverWait(self.driver, self.timeout).until(
                EC.presence_of_element_located(locator)
            )
            logger.info(f"成功定位元素:{locator}")
            return element
        except Exception as e:
            logger.error(f"元素定位失败:{locator},原因:{str(e)}", exc_info=True)
            raise

    # 定位多个元素
    def find_elements(self, locator):
        try:
            elements = WebDriverWait(self.driver, self.timeout).until(
                EC.presence_of_all_elements_located(locator)
            )
            logger.info(f"成功定位多个元素:{locator},共{len(elements)}个")
            return elements
        except Exception as e:
            logger.error(f"多个元素定位失败:{locator},原因:{str(e)}", exc_info=True)
            raise

    # 输入操作
    def input_text(self, locator, text):
        try:
            element = self.find_element(locator)
            element.clear()  # 先清空输入框
            element.send_keys(text)
            logger.info(f"在元素{locator}中输入文本:{text}")
        except Exception as e:
            logger.error(f"输入文本失败:{str(e)}", exc_info=True)
            raise

    # 点击操作
    def click_element(self, locator):
        try:
            element = WebDriverWait(self.driver, self.timeout).until(
                EC.element_to_be_clickable(locator)
            )
            element.click()
            logger.info(f"成功点击元素:{locator}")
        except Exception as e:
            logger.error(f"点击元素失败:{str(e)}", exc_info=True)
            raise

    # 获取页面标题
    def get_title(self):
        title = self.driver.title
        logger.info(f"当前页面标题:{title}")
        return title

    # 获取元素文本
    def get_element_text(self, locator):
        text = self.find_element(locator).text
        logger.info(f"元素{locator}的文本内容:{text}")
        return text

    # 页面刷新
    def refresh(self):
        self.driver.refresh()
        logger.info("页面已刷新")
2. 业务页面类page/baidu_page.py

百度首页 为例,封装百度首页的元素定位业务操作 (如搜索、点击百度一下),继承BasePage,直接使用通用操作方法:

python 复制代码
from selenium.webdriver.common.by import By
from page.base_page import BasePage

class BaiduPage(BasePage):
    # ********** 页面元素定位 **********
    # 搜索输入框
    SEARCH_INPUT = (By.ID, "kw")
    # 百度一下按钮
    SEARCH_BUTTON = (By.ID, "su")
    # 搜索结果标题(第一个)
    SEARCH_RESULT_FIRST = (By.XPATH, '//div[@id="content_left"]//h3[1]/a')

    # ********** 页面业务操作 **********
    def search(self, keyword):
        """百度搜索操作:输入关键词+点击百度一下"""
        self.input_text(self.SEARCH_INPUT, keyword)
        self.click_element(self.SEARCH_BUTTON)

    def get_first_result_title(self):
        """获取第一个搜索结果的标题"""
        return self.get_element_text(self.SEARCH_RESULT_FIRST)

四、测试数据模块(data)

采用YAML 实现数据驱动 ,将测试用例的入参、预期结果分离到数据文件中,支持多组用例数据批量执行,创建data/test_data.yaml

bash 复制代码
# 百度搜索测试数据
baidu_search:
  - case_name: 搜索Python关键词
    keyword: Python
    expected_title: Python_百度百科
  - case_name: 搜索Selenium关键词
    keyword: Selenium
    expected_title: Selenium_百度百科
  - case_name: 搜索Pytest关键词
    keyword: Pytest
    expected_title: pytest_百度百科

五、Pytest 全局配置(conftest.py & pytest.ini)

1. 全局夹具conftest.py

Pytest 核心,实现浏览器驱动的全局初始化 / 销毁用例失败自动截图页面类实例化 ,夹具作用域为function(每个用例执行前初始化,执行后销毁),所有用例可直接调用夹具,无需重复初始化:

python 复制代码
import pytest
from _pytest.fixtures import FixtureRequest
from selenium.webdriver.remote.webdriver import WebDriver
from page.baidu_page import BaiduPage
from utils.driver_util import DriverUtil
from utils.screenshot_util import screenshot_util
from utils.log_util import logger

# ********** 全局夹具:浏览器驱动 **********
@pytest.fixture(scope="function")
def driver() -> WebDriver:
    # 用例执行前:初始化驱动
    dr = DriverUtil.get_driver()
    yield dr  # 返回驱动,供用例调用
    # 用例执行后:退出驱动
    DriverUtil.quit_driver(dr)

# ********** 全局夹具:百度页面实例 **********
@pytest.fixture(scope="function")
def baidu_page(driver):
    # 传入驱动,实例化百度页面类,供用例直接调用
    return BaiduPage(driver)

# ********** Pytest钩子:用例失败自动截图 **********
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    # 获取用例执行结果
    outcome = yield
    rep = outcome.get_result()
    # 仅当用例执行失败时截图(call.when == "call" 表示用例执行阶段)
    if rep.when == "call" and rep.failed:
        try:
            # 获取驱动对象(从用例的夹具中获取)
            driver = item.funcargs["driver"]
            # 用例名
            case_name = item.nodeid.split("::")[-1]
            # 执行截图
            screenshot_util.take_screenshot(driver, case_name)
        except Exception as e:
            logger.warning(f"用例失败截图时发生异常:{str(e)}")
2. Pytest 全局配置pytest.ini

配置 Pytest 的用例搜索规则报告生成失败重跑日志级别等,执行用例时自动加载:

bash 复制代码
[pytest]
# 测试用例目录
testpaths = test_case
# 用例文件匹配规则
python_files = test_*.py
# 用例类匹配规则
python_classes = Test*
# 用例方法匹配规则
python_functions = test_*
# 失败重跑次数
reruns = 1
# 重跑间隔(秒)
reruns_delay = 2
# 日志级别
log_level = INFO
# 禁用警告
addopts = -q --disable-warnings
# HTML报告配置(生成带截图的报告)
addopts += --html=./report/report.html --self-contained-html

六、测试用例层(test_case)

创建测试用例test_case/test_baidu.py,调用页面类的业务方法数据驱动的测试数据 ,使用 Pytest 的@pytest.mark.parametrize实现多组数据批量执行,用例中仅关注业务逻辑,不关注底层 UI 操作:

python 复制代码
import pytest
import yaml
import os
from utils.log_util import logger

# 读取测试数据(YAML)
DATA_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "test_data.yaml")
with open(DATA_FILE_PATH, encoding="utf-8") as f:
    test_data = yaml.safe_load(f)["baidu_search"]

class TestBaidu:
    """百度搜索测试用例类"""

    @pytest.mark.parametrize("case", test_data)
    def test_baidu_search(self, baidu_page, case):
        """
        百度搜索测试用例
        :param baidu_page: 百度页面夹具(已实例化)
        :param case: 测试数据(每组用例的关键词、预期结果)
        """
        logger.info(f"开始执行用例:{case['case_name']}")
        # 1. 执行百度搜索
        baidu_page.search(case["keyword"])
        # 2. 获取第一个搜索结果标题
        actual_title = baidu_page.get_first_result_title()
        # 3. 断言:实际结果包含预期结果
        assert case["expected_title"] in actual_title, \
            f"用例{case['case_name']}执行失败!预期:{case['expected_title']},实际:{actual_title}"
        logger.info(f"用例{case['case_name']}执行成功")

if __name__ == "__main__":
    pytest.main(["-s", __file__])

七、框架运行入口(run.py

创建一键运行脚本,无需在终端输入复杂命令,直接执行python run.py即可运行所有用例生成测试报告记录日志失败截图

python 复制代码
import pytest
import os
from utils.log_util import logger

if __name__ == "__main__":
    logger.info("="*50 + "UI自动化测试框架开始执行" + "="*50)
    # 执行Pytest用例,使用pytest.ini的配置
    pytest.main()
    # 测试报告路径
    report_path = os.path.join(os.path.dirname(__file__), "report", "report.html")
    logger.info(f"="*50 + f"测试执行完成,测试报告路径:{report_path}" + "="*50)

框架运行与验证

1. 直接运行

在框架根目录执行命令:

python

python 复制代码
python run.py

或在终端执行 Pytest 命令:

pytest

python 复制代码
pytest

2. 运行结果验证

  1. 日志logs/目录下生成日期 + log的日志文件,控制台也会输出日志,包含浏览器启动、用例执行、截图等信息;
  2. 截图 :若用例失败,report/screenshot/目录下生成日期 + 用例名的截图,成功用例不截图;
  3. 测试报告report/目录下生成report.html可视化报告,包含用例执行结果执行时间失败原因截图链接,可直接用浏览器打开;
  4. 控制台输出:显示用例执行数量、成功 / 失败 / 跳过数、重跑数等。

核心特性

  1. 分层解耦:PO 模式 + 工具类 + 用例层,各模块独立,维护成本低;
  2. 配置驱动 :全局配置集中在config.ini,无需修改代码即可切换浏览器、修改网址;
  3. 数据驱动:基于 YAML 实现多组用例数据批量执行,支持 JSON/Excel 扩展;
  4. 自动保障:失败重跑、用例失败自动截图、显式等待,提升用例稳定性;
  5. 可视化报告:HTML 报告包含截图和失败原因,便于问题排查;
  6. 日志完善:控制台 + 文件双输出,分级记录,关键操作全日志。

常见问题解决

  1. 浏览器启动失败 :检查浏览器版本是否与webdriver-manager适配,或切换浏览器(edge/firefox);
  2. 元素定位失败:检查元素定位器是否正确(ID/XPATH 是否变化),延长超时时间;
  3. 截图失败:检查驱动对象是否正确传入,截图路径是否有写入权限;
  4. YAML 数据读取失败:检查 YAML 语法是否正确(缩进、冒号),文件编码是否为 UTF-8;
  5. 报告生成失败 :检查report/目录是否有写入权限,或升级pytest-html版本。
相关推荐
Wpa.wk3 小时前
Docker原理和使用场景(网络模式和分布式UI自动化环境部署)
linux·经验分享·分布式·测试工具·docker·性能监控
我送炭你添花3 小时前
软件测试为何不可或缺?——以复杂宏系统与 PTZ 控制为例,深度解析 pytest 的实战价值与不可替代性
python·测试工具·pytest
深蓝电商API5 小时前
Selenium 截图与元素高亮定位技巧
爬虫·python·selenium
我的xiaodoujiao19 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 43--添加allure测试报告显示信息和其他封装方法
python·学习·测试工具·allure
夜郎king1 天前
Java搭配Selenium:实现网页访问与自动截图的实战指南及常见问题解析
selenium·java 网页截图·selenium截图实现
深蓝电商API1 天前
Selenium 爬取 Canvas 渲染的数据图表
爬虫·python·selenium
可可南木1 天前
3070文件格式--10--testorder文件格式详解
功能测试·测试工具·pcb工艺
深蓝电商API1 天前
Selenium 动作链 ActionChains 高级用法
爬虫·python·selenium