UI自动化框架封装实战:分层设计、页面对象与稳定性实践

发布时间:2026/10/14 20:16:16
UI自动化框架封装实战:分层设计、页面对象与稳定性实践
简介这是一套面向测试开发工程师与自动化测试初学者的UI自动化封装框架基于Python与Pytest构建采用Page Object模式简化分层设计覆盖Web与桌面端常用操作并将数据驱动、断言增强、失败重试、测试报告生成及CI/CD集成等能力整合为易调用的接口。资源共2000个文件压缩包68.95MB主体为1870个py测试脚本与封装模块另含少量c/h扩展实现、txt说明文档、json/xml配置文件及markdown笔记便于二次复用或定制适合作为企业内部测试平台或个人学习参考。目前已有171人学习下载。读者可获得完整可运行的自动化项目骨架、分层封装思路、pytest插件搭配方案以及异常处理与并发执行等落地细节目录结构清晰能快速定位基类封装、页面对象、用例集与报告输出模块有助减少自定义框架的重复造轮子成本。1. 自己封装组建的UI自动化为什么我劝你别再堆脚本UI自动化这件事脚本刚写完那周是绿的等被测系统迭代两个版本就开始全屏飘红。你花一下午排查发现不过是登录按钮的样式类从btn-primary改成了btn-brand定位器集体失效。我见过太多项目死在维护阶段——不是用例没写好而是把最不该重复的逻辑散落到了几十个脚本里。自己封装组建UI自动化框架不是为了炫技而是要把「会变的东西」收拢到少数几个文件里驱动初始化、元素定位策略、等待时机、失败留证全部统一出口。这份资源面向的正是两类人刚上手想少走弯路的以及被脚本维护拖垮、想重建体系的。接下来我从骨架开始一步步把这套框架拆给你看。2. 框架分层与工程骨架为什么先拆目录比先写脚本更重要自封装UI自动化最容易翻车的点不在技术而在工程结构。我见过某跨平台系统的自动化仓库跑了两年后没人敢动元素id散落在几十个功能脚本里改一个按钮的定位符要全局搜索加逐个试跑。封装的第一步不是写代码而是画边界。2.1 分层原则config、common、pages、testcase各管哪一段自封装UI自动化框架的骨架通常分成四层加一个报告目录职责边界画清楚后面所有脚本都是往这个框子里填东西。下面是我在模拟项目X里验证过的划分方式。层职责放什么禁止放什么config可变参数唯一出口环境地址、浏览器类型、超时时间、账号数据元素定位符属于页面对象层common工具与基类驱动工厂、日志封装、BasePage具体业务动作pages页面要素与操作元素定位符、页面动作方法断言、流程编排testcase用例与数据编排测试步骤、断言、参数化数据元素定位、驱动初始化reports过程产物日志文件、失败截图、报告html源码文件这里最容易被忽略的是单向依赖规则testcase可以调pagespages可以调commoncommon可以调config反向路径一律不允许。一旦出现pages里去读配置文件、testcase里直接写find_element分层就名存实亡。实际维护中你会遇到「改一个超时时间要动五个文件」的情况那不是参数多是依赖方向错了。config层要单独拎出来说。环境地址、浏览器类型、隐式等待秒数、账号密码这类东西本质都是「换环境就要改」的变量。把它们写进yaml或ini文件而不是硬编码在脚本里是为了让同一个用例集能在测试环境、预发环境来回跑。我一般会在config里放一个env.ini里面按[test]、[staging]分节运行用例时通过--env参数指定加载哪一节。这样预发环境的地址变了改的只有配置文件用例代码一行不用动。2.2 可执行步骤先搭骨架再写实现拿到这份资源后第一步不是去看驱动怎么封装而是先把目录结构立起来。空目录不亏后面每一块代码都是往对应位置填。骨架如下auto_ui/ ├── config/ │ ├── __init__.py │ ├── env.ini │ └── settings.py ├── common/ │ ├── __init__.py │ ├── logger.py │ ├── driver_factory.py │ └── base_page.py ├── pages/ │ ├── __init__.py │ └── login_page.py ├── testcase/ │ ├── __init__.py │ └── test_login.py ├── reports/ │ ├── logs/ │ └── screenshots/ └── pytest.ini每个目录新建后在__init__.py里留一行注释说明「这个包只允许被谁引用」。这个动作看起来刻意但三个月后你回来看会发现它挡住了不少随手乱import。config/settings.py的作用是把ini文件解析成Python对象业务代码里不直接读文件而是from config.settings import ENV_CONFIG换配置源时只改这一处。pytest.ini是这个骨架的黏合剂我用它替代了conftest.py里大量重复的config初始化[pytest] testpaths testcase addopts -v -s --tbshort markers p0: 核心冒烟用例 p1: 主流程回归用例testpaths显式指定用例目录避免pytest跑去扫描pages和common里的辅助类addopts里的--tbshort是我踩坑后加上的——默认的长回溯会把定位失败信息刷到一屏之外short模式只保留最近几层调用失败原因一眼能看见。markers提前声明p0/p1两级标签配合-m p0可以只跑冒烟集。3. 驱动层的封装初始化复用与三个翻车点所有用例都依赖driver实例。如果每个脚本都自己写webdriver.Chrome(options...)就等于把可变点铺满了整个仓库。驱动这一层必须收口收口之后你改浏览器版本、切headless、换远程节点都只需要动一个文件。3.1 驱动工厂把浏览器选择、启动参数、等待策略放在同一个出口我一般用一个简单的函数做驱动工厂不刻意上类函数签名够用了。核心思路是浏览器类型和启动参数全部来自config函数内部只做组装和异常兜底。# common/driver_factory.py from selenium import webdriver from config.settings import ENV_CONFIG def create_driver(browser: str None, headless: bool None): 创建WebDriver实例浏览器类型与headless模式支持运行时覆盖 browser browser or ENV_CONFIG[browser] headless headless if headless is not None else ENV_CONFIG[headless] if browser chrome: options webdriver.ChromeOptions() if headless: options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-gpu) options.add_argument(--window-size1920,1080) driver webdriver.Chrome(optionsoptions) elif browser firefox: driver webdriver.Firefox() else: raise ValueError(f不支持的浏览器类型: {browser}) driver.implicitly_wait(ENV_CONFIG[timeout]) driver.maximize_window() return driver参数说明browser和headless都支持显式覆盖这样在调试时可以直接跑create_driver(headlessTrue)不用改配置文件。--headlessnew是Chrome新版的推荐写法老写法--headless在新版本上会报兼容警告。--no-sandbox和--disable-gpu是容器环境里必加的不加会在无桌面环境下启动失败或渲染异常。真正要注意的是隐式等待和显式等待的叠加问题。implicitly_wait(5)对每次find_element都生效而显式等待只管当前这一次查找。两者同时存在时最坏等待时间是两者之和隐式5秒加显式10秒一个元素消失时你要干等15秒才能报失败。所以在BasePage里做显式等待之前我会先把这个逻辑想清楚——要么统一用显式等待要么保留一个2到3秒的隐式等待兜底不混用大数值。3.2 fixture与浏览器生命周期什么时候开、什么时候关驱动创建好之后还要解决「用例怎么拿driver、用完怎么还」的问题。pytest的fixture是标准答案不是每个用例写try-finally去手动quit。# conftest.py import pytest from common.driver_factory import create_driver pytest.fixture(scopefunction) def driver(): driver create_driver() yield driver driver.quit()我推荐scopefunction起步也就是每个用例都开一个全新浏览器。这种模式最大的优点是隔离用例A把登录态搞坏了不影响用例B。缺点是慢一个用例多花3到5秒启动时间。等用例集大到上百条之后可以再升级到scopesession配合会话清理但那样会引入一个很隐蔽的坑某个用例异常退出后浏览器留存脏状态后面所有用例都跟着失败。新手阶段、用例量在50条以内function级别是最省心的。在Web和App混合的项目里驱动工厂的入参换成Appium的capabilities即可工厂逻辑本身不用动。这就是把驱动收口的红利——你在页面层写的那些方法跨端可以直接复用区别只在driver的创建方式。4. 页面对象与用例编排把元素、动作、数据从用例里剥出来装好驱动、跑通一个「打开浏览器-输入账号-点登录」的用例并不难难在当被测系统有20个页面时你的定位符、动作方法、测试数据如果不分层仓库会迅速退化成脚本垃圾场。页面对象模式Page Object就是用来解决这个问题的。4.1 BasePage把元素定位、动作、等待三种语义分开收口基础实现不复杂关键是把「等待」和「动作」绑定起来不是先等元素存在再单独执行点击而是每个动作自带合适的等待条件。以下是这份资源里BasePage的核心部分# common/base_page.py from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver driver self.timeout 5 def find_element(self, locator, timeoutNone): timeout timeout or self.timeout return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator), messagef元素定位超时: {locator} ) def click(self, locator, timeoutNone): timeout timeout or self.timeout el WebDriverWait(self.driver, timeout).until( EC.element_to_be_clickable(locator), messagef元素不可点击: {locator} ) el.click() return self def input_text(self, locator, text, timeoutNone): timeout timeout or self.timeout el WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator), messagef输入框不可见: {locator} ) el.clear() el.send_keys(text) return self这里三个等待条件是有讲究的不能统一用一个presence_of_element_located。点击操作要求元素在DOM里且可被点击如果用一个只验证存在的等待按钮还在渲染中就被点击了偶发点击无效输入框要可见才能接收键盘事件不可见时send_keys会把事件丢进空气。return self的链式风格让页面动作写起来像说话login_page.input(...).click(...).wait_until(...)省掉大量临时变量。timeout参数的设计是留给慢接口页面的后门。默认5秒够覆盖大多数场景碰到报表加载、数据量大的列表页调用时显式传timeout15即可不用改基类。4.2 页面类与用例层一个登录用例跑通全链路页面类的要点是把定位符定义为类属性集中在文件顶部。任何元素变了打开这个文件只改一行而不是去全仓库搜让定位符租期达到最长# pages/login_page.py from common.base_page import BasePage from selenium.webdriver.common.by import By class LoginPage(BasePage): user_input (By.CSS_SELECTOR, input[data-testidusername]) pwd_input (By.CSS_SELECTOR, input[data-testidpassword]) login_btn (By.CSS_SELECTOR, button[data-testidlogin]) def login(self, username, password): self.input_text(self.user_input, username) self.input_text(self.pwd_input, password) self.click(self.login_btn) return self定位符格式统一用(By.CSS_SELECTOR, input[data-testidusername])二元组直接喂给BasePage的等待条件不需要再拆包。这个资源里我刻意没有用xpath绝对路径——/html/body/div[3]/form/input[1]这类写法是定位符翻车的头号来源前面加一个节点整个路径就断掉。># testcase/test_login.py import pytest from pages.login_page import LoginPage pytest.mark.p0 def test_login_success(driver): homepage LoginPage(driver).open().login(admin, test123) assert homepage.is_user_logged_in() is True pytest.mark.parametrize(username,password, [ (admin, wrong_password), (nonexist_user, test123), ]) def test_login_failed_with_bad_credentials(driver, username, password): login_page LoginPage(driver).open() login_page.login(username, password) assert login_page.get_error_message() is not Noneparametrize这份资源里是重点但也是易错点。数据驱动适合登录、搜索这类「同一套动作换不同输入」的场景但如果是「五个用例动作顺序完全不同」就老老实实拆成五个函数别硬塞到参数化里。参数化用例失败定位时报表信息会比较模糊你看到的是一条用例带多个参数组得展开才看得见哪一组炸了所以给每组参数加idpytest.mark.parametrize(..., ids[wrong_pwd, no_user])失败信息会清晰得多。5. UI自动化的坑定位策略失效、测试顺序依赖与丢日志框架搭完跑绿不难难在跑稳定。我整理了封装过程中踩过的五个高频现场每一条都是「现象 → 原因 → 解决」的完整链路。这不是理论推演是实打实翻车翻出来的经验。5.1 定位器昨天还能点今天就连不上了现象前一天跑得好好的用例第二天一上来就在某个按钮上超时失败日志显示element_to_be_clickable等待超时。手动打开页面元素明明就在那里。原因这个元素的定位符用的是样式类或DOM结构中的某个不稳固属性比如button.btn.btn-primary。前端上次发版改了样式类名或者加了层嵌套类名还在但样式优先级变了Selenium找不到完全匹配的selector。解决把定位符换成># conftest.py 追加 import time from pathlib import Path import pytest pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver: shot_dir Path(reports/screenshots) shot_dir.mkdir(parentsTrue, exist_okTrue) name f{item.name}_{time.strftime(%Y%m%d_%H%M%S)}.png driver.save_screenshot(str(shot_dir / name))这个钩子的关键点在于判断report.when call。整个用例有setup、call、teardown三个阶段只对call阶段失败截图避免把fixture的启动错误误判为用例失败、截一张空白图。item.funcargs.get(driver)拿到的是当前用例的driver实例如果你把fixture叫了别的名字这里要改成对应的名字。截图之外我还会在日志里记录失败时的页面上下文——当前URL、页面标题、错误消息摘要。截图是给你看画面上下文是给定位问题用的。我自己在维护框架时立了一条规矩凡是新增一个页面类必须同时确认failure hook已经生效不生效不允许提交用例。从那以后每次接手新框架第一件事就是检查失败留证挂没挂上没挂就先补不补不跑。否则你跑得再勤遇到玄学失败还是两眼一抹黑。希望帮到你。本文还有配套的精品资源点击获取