本次搭建的是轻量可扩展的 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 # 框架运行入口(一键执行用例+生成报告)
- 安装依赖包
创建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. 运行结果验证
- 日志 :
logs/目录下生成日期 + log的日志文件,控制台也会输出日志,包含浏览器启动、用例执行、截图等信息; - 截图 :若用例失败,
report/screenshot/目录下生成日期 + 用例名的截图,成功用例不截图; - 测试报告 :
report/目录下生成report.html可视化报告,包含用例执行结果 、执行时间 、失败原因 、截图链接,可直接用浏览器打开; - 控制台输出:显示用例执行数量、成功 / 失败 / 跳过数、重跑数等。
核心特性
- 分层解耦:PO 模式 + 工具类 + 用例层,各模块独立,维护成本低;
- 配置驱动 :全局配置集中在
config.ini,无需修改代码即可切换浏览器、修改网址; - 数据驱动:基于 YAML 实现多组用例数据批量执行,支持 JSON/Excel 扩展;
- 自动保障:失败重跑、用例失败自动截图、显式等待,提升用例稳定性;
- 可视化报告:HTML 报告包含截图和失败原因,便于问题排查;
- 日志完善:控制台 + 文件双输出,分级记录,关键操作全日志。
常见问题解决
- 浏览器启动失败 :检查浏览器版本是否与
webdriver-manager适配,或切换浏览器(edge/firefox); - 元素定位失败:检查元素定位器是否正确(ID/XPATH 是否变化),延长超时时间;
- 截图失败:检查驱动对象是否正确传入,截图路径是否有写入权限;
- YAML 数据读取失败:检查 YAML 语法是否正确(缩进、冒号),文件编码是否为 UTF-8;
- 报告生成失败 :检查
report/目录是否有写入权限,或升级pytest-html版本。