简介这是一套基于Python Pytest与Page Object模式封装的自定义UI自动化测试框架适合测试人员与开发者在Web或桌面应用项目中快速落地界面回归测试。包内包含2000个文件以py测试脚本为主辅以c/h底层扩展、json/xml配置、少量js与文档文件压缩后约68.95MB结构上覆盖测试用例、封装方法、数据驱动与报告集成等模块。目前已有171人学习使用。通过该包可复用封装好的页面对象与断言逻辑理解pytest夹具、参数化、异常处理及覆盖率统计的配合方式并能在此基础上按项目需要扩展Selenium或PyAutoGUI操作节省从零搭建UI自动化框架的时间适合具备一定Python基础、希望提升测试效率的团队或个人参考。 去年我把手头的UI自动化整个推翻重写了一遍核心就干了一件事把散落在各个用例里的定位、点击、输入、等待全部收拢封装成统一的一套组建。这个决定让脚本维护成本降了不止一半也让新同学从“看不懂测试代码”变成了“半天就能上手写用例”。今天想把这套封装思路完整拆一遍从为什么封装、基类怎么设计、Page Object怎么做到稳定性踩坑和AI辅助提效都会聊到。这篇内容主要面向已经写过一些UI自动化、但还没系统整理过框架的测试开发同学如果你刚接触Web或App自动化也能从中拿到一套可以直接落地的封装路径。项目的实际背景是Web端业务系统先跑通后来同一套基座扩展到了Android和iOS所以我下面会兼顾Selenium、Appium以及多端复用的方案。1. 封装前先想清楚这三件事1.1 你究竟要稳定还是要速度很多团队做UI自动化一上来就追求“把全流程都自动化”结果就是每天被一堆随机失败打脸。我自己的经验是自动化解决的是回归验证的确定性不是覆盖所有功能的速度。封装做得好不好第一判断标准不是“写了多少条用例”而是“这个用例跑挂了你能不能在三分钟内知道它为什么挂”。所以封装之前先给自动化定一个边界核心主流程、高频回归、跨端一致性验证这些值得做UI频繁改版、一次性的临时活动页面不值得花大力气写进框架。封装的价值不是把代码变多而是把重复劳动变成一次性的约定。后面基类、Page Object、数据驱动全都是围绕“稳定优先”来设计的。1.2 框架分层一个不被烂代码拖垮的结构如果所有脚本都直接写driver.find_element短时间跑得通项目一大人就废了。我最终采用的框架结构可以简化为五层project/ ├── cases/ # 测试用例层只写数据、动作、断言 ├── page_objects/ # Page Object 层一个页面一个类 ├── components/ # 公共业务组件如登录框、弹窗、Toast ├── base/ # 基类封装如 BasePage、DriverFactory ├── utils/ # 公共工具日志、截图、读取数据 └── config/ # 环境配置、元素定位器这个分层最核心的原则是“从下往上依赖”。用例层不直接接触Selenium/Appium原生的find_element只调PageObject的方法PageObject不关心测试数据怎么来只负责页面操作基类不关心业务只解决driver、等待、异常处理这些通用问题。这样做的好处是接手的人不需要看所有代码改一个弹窗组件不影响其他用例换浏览器、换设备也只需动DriverFactory。1.3 选型Selenium、Appium与pytest的组合为什么够用我选的组合很简单Web端Selenium 4App端Appium 2底层统一用pytest做执行框架配合allure出报告。有人会问为什么不用Cypress或者Playwright原因是项目当时既有Web又有AppSelenium和Appium能共享同一套Page Object设计思量而且团队里已有的基础设施、定位习惯、驱动管理方式都更成熟。Appium的底层是WebDriver协议和Selenium同源所以BasePage里大部分方法能直接复用。唯一要注意的是App和Web在操作语义上的差异比如Web有iframeApp有native页面和WebView切换这些差异不能靠正常继承硬吞需要在基类里留出扩展点。pytest的fixture机制对driver初始化和回收特别友好conftest里统一做driver管理比在用例里写setup/teardown干净得多。2. 把重复劳动封装进基类2.1 基类BasePage的核心职责BasePage是整个封装的地基。它不解决业务问题只解决三件事统一找元素、统一等待、统一异常处理。我见过很多人把find_element封装成一行return self.driver.find_element(*locator)这种封装等于没封装因为调用方还是得自己处理找不到元素、等待不够、日志不清晰这些问题。我的BasePage大致长这样class BasePage: def __init__(self, driver): self.driver driver self.timeout 10 def find_element(self, locator, timeoutNone): timeout timeout or self.timeout try: element WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) self._log(f元素定位成功: {locator}) return element except Exception as e: self.screenshot(find_element_error) raise AssertionError(f定位元素失败 {locator}: {e}) def click(self, locator, timeoutNone): element self.find_element(locator, timeout) try: element.click() self._log(f点击元素: {locator}) except Exception as e: self.screenshot(click_error) raise AssertionError(f点击失败 {locator}: {e}) def input_text(self, locator, text, timeoutNone): element self.find_element(locator, timeout) element.clear() element.send_keys(text) self._log(f输入文本: {locator} - {text})这版封装里有个容易被忽略的细节每步操作前统一走显式等待而不是只依赖隐式等待。元素找不到时立刻截图断言消息里包含locator和异常信息这比直接抛出NoSuchElementException要直观得多。实际跑脚本时一张失败截图加一行日志能省掉不少排查时间。2.2 元素定位与操作层的封装细节我不建议把定位表达式散落在用例或PageObject的每个方法里而是集中管理。定位器用元组统一保存例如(id, login_btn)这样BasePage里接收locator不需要关心是ById还是ByXPath也方便后续统一改定位策略。操作层有几个容易踩的坑值得展开说一下。第一click前不一定要等待“可点击”要等待“可见且可用”因为按钮在动画过程中会存在看得见但点不中的状态第二input前调用clear不一定清干净所有场景有的框架会触发输入事件这时需要ctrla再删除第三Web端偶尔会遇到页面被遮罩层挡住点击App端会停在半屏弹层解决思路是先判断元素是否可点击不可点击就先处理遮罩。我通常会在BasePage里再补两个基础方法is_element_visible和swipe_to_element分别用来快速判断状态和处理App长列表。这些看起来琐碎但正是这些琐碎细节决定了框架是“能跑”还是“稳”。2.3 多端支持Web和App怎么共用一套逻辑多端复用的关键是“不要让PageObject直接创建driver”driver从外面传进来。这样同一个LoginPage既能接收ChromeDriver也能接收AppiumDriver。我服务的项目里Android和iOS大都沿用这一套但Android用uiautomator2驱动iOS用XCUITest驱动差异由DriverFactory屏蔽掉了。DriverFactory的基本思路是class DriverFactory: staticmethod def get_driver(platform, config): if platform web: return create_web_driver(config) elif platform android: return create_android_driver(config) elif platform ios: return create_ios_driver(config)WebDriverManager在这里很关键它可以根据本机浏览器版本自动下载匹配的驱动省去手动配置浏览器驱动版本的痛苦。App端则需要维护capabilities配置包括deviceName、platformVersion、appPackage、appActivity这些统一放在config里不要散落在各用例中。3. 页面对象与业务组件的封装实践3.1 Page Object模式封装的不是代码而是抽象Page Object好像是老生常谈但很多人做成了“把定位器从用例搬到类里”方法体还是click、input等裸调用。这其实只完成了一半。PageObject真正的价值是把页面当作一个对象暴露给用例的是“用户能做的动作”而不是“元素怎么操作”。比如登录页一个好的PageObject应该是这样class LoginPage(BasePage): username_input (id, username) password_input (id, password) login_button (id, login_btn) error_tip (xpath, //div[classerror]) def login(self, username, password): self.input_text(self.username_input, username) self.input_text(self.password_input, password) self.click(self.login_button) def get_error_tip(self): return self.get_text(self.error_tip)用例层调用时完全不需要关心元素定位是id还是xpath也不需要知道输入框要先清空再输入。这样的抽象换UI的时候只需改PageObject类用例基本不动。这才是“封装”两个字的意义。3.2 组件化封装登录、列表、弹窗这些公共模块项目里总会有一些跨页面复用的东西最典型的是弹窗、Toast、下拉刷新、搜索栏。如果每个页面都写一套改文案、改按钮位置时你会发现全工程都在飘红。组件化封装就是把这些公共模块从PageObject里抽出来让不同页面共用同一个操作逻辑。我以弹窗为例很多业务页面的弹窗结构都类似只有标题和确认按钮。封装后可以做一个CommonDialog组件提供confirm()、cancel()、get_title()等方法。然后你在页面里可以通过组合方式使用它class SomePage(BasePage): dialog CommonDialog(self.driver)这样写的好处非常直接弹窗的关闭策略、等待策略、遮罩判断只维护一份。Android和iOS的弹窗样式不同但都可以在CommonDialog内部按平台做分支。封装组件不是在代码层面炫技而是把容易变化的地方盯住收敛到一个文件里。3.3 数据驱动与用例层的“薄化”用例层越薄后期维护越省心。我的习惯是把测试数据放到外部文件用例只负责“取数据、调动作、断结果”。比如登录用例用pytest.mark.parametrize配合yaml数据能写得很干净class TestLogin: pytest.mark.parametrize(case, load_yaml(cases/login.yaml)) def test_login(self, case, driver): login_page LoginPage(driver) login_page.login(case[username], case[password]) if case.get(expect_error): assert case[expect_error] in login_page.get_error_tip() else: assert HomePage(driver).is_loaded()数据文件里只需要写username、password、expect_error这些字段。新增一条用例数据不需要碰代码而且这些yaml还可以复用给接口自动化做数据准备。顺便说一句这里的数据驱动也是某种程度的“接口封装”它把UI操作和测试数据彻底隔离开数据变化不会引起代码变化。4. 稳定性与执行效率封装最容易踩的坑4.1 显式等待、隐式等待与轮询别混用UI自动化最常见的失败就是“元素没找到”和“元素不可点击”。问题往往出在等待策略上。我强烈建议整个框架里只用显式等待关掉隐式等待不要让它们混在一起。隐式等待是给driver设置一个全局轮询但点击前“可见不可点击”这类复杂场景它处理不了显式等待则可以针对单个条件做精确控制。等待方式适用范围容易踩的坑time.sleep临时调试睡太长拖慢执行睡太短不稳定隐式等待全局粗粒度兜底和显式等待混用会放大等待时间显式等待元素可见、可点、消失需要选对expected_conditions轮询重试网络慢、异步加载必须限制重试次数避免死循环我常用的封装是先从WebDriverWait和expected_conditions组合再加上一个简单的重试装饰器。凡是遇到接口返回慢导致页面局部刷新的场景不要盲目调大timeout而是要定位到“真正要找的元素在哪个条件后会稳定出现”然后针对它写等待。4.2 驱动版本、浏览器环境与远程执行浏览器驱动版本不匹配是所有Web自动化新手的噩梦。手动下载驱动再放到PATH里换台电脑就崩根本谈不上团队协作。现在我会用WebDriverManager几行代码就能自动匹配Chrome版本。同理Appium也需要明确Android和iOS的不同驱动依赖Android的uiautomator2、iOS的XCUITest建议在安装文档里写清楚避免每个人环境不一样导致结果不一致。当用例量上来以后单机执行会越来越慢。我的建议是在封装里提前预留远程执行开关本地调试用本机driverCI或集中跑的时候走Selenium Grid或Appium Server的remote地址。这样从单机扩展到并行不会有很大的改造量。4.3 用例失败与截图日志封装里最容易被忽略的部分很多框架能跑但失败时只抛一个NoSuchElementException连元素长什么样都不知道。我的BasePage里每步操作都写日志并在异常时自动截图。pytest的钩子函数会进一步把截图和日志附到allure报告上这样失败分析基本不用看控制台。pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver: driver.save_screenshot(freports/fail_{item.name}.png)这里的一个经验心得是截图命名最好包含用例名和时间戳否则跑一遍用例失败多张图根本分不清谁是谁。日志也不能只记“定位成功”或“点击成功”最好带上页面当前URL或当前Activity这样多端并行时能快速定位是哪个环境出的问题。下面整理一个我在实际维护中反复遇到的排查速查表问题常见原因处理建议元素定位不到页面没加载完 / iframe未切换显式等待 切换iframe后再查找点击时报被遮挡弹窗、广告遮住元素封装清理遮罩逻辑或先关闭弹窗Android执行比iOS慢网络/动画/设备性能差异针对不同平台设置合理等待时间本地跑过CI跑不过浏览器参数、屏幕分辨率不同远程执行时统一capabilities和视口尺寸用例间数据互相影响没有独立测试账号数据驱动时每次生成新数据或做清理5. 给自己一点提效空间AI辅助与录制工具的价值5.1 录制生成脚本只能当草稿不能当成品现在有不少工具支持录制操作并自动生成UI自动化脚本确实能快速摸清系统的元素路径。但我试过之后发现录制出来的脚本有个共同问题定位器太死板等待方式几乎没有遇到一点动态变化就挂。录制工具最大的价值是帮我们批量摘取页面元素信息省去人工从DOM树里找定位器的过程而不是直接生成可长期维护的用例。我现在的做法是用录制工具跑一遍主流程导出脚本后把定位器整理到PageObject里然后把裸的click/input替换成BasePage封装方法补上显式等待和日志。这个过程看似多了一步实际上等于把录制的“草稿”重构成“真框架”比从零开始写要快很多。5.2 用AI补用例、改定位器能省多少事近一年AI写UI自动化的提效越来越明显。我最常用的场景是让它根据业务描述生成一套测试用例模板或者给它一段重复性很强的PageObject让它补全类似页面的代码。AI对“封装继承多态”这类结构理解得比一般人想象中好它可以帮你快速搭出统一的类结构省去敲重复代码的时间。但我也要提醒一点AI生成的定位器和业务断言不一定准确尤其对复杂业务状态的理解有偏差。它更适合做“初稿生成、批量修改、重构辅助”不能完全替代人的判断。每次让AI写代码我都会审视三件事等待方式是否合理、失败信息是否清楚、用例之间有没有隐藏的状态依赖。我个人这几年最大的体会是封装到最后真正值钱的不是那几层代码而是你对被测系统的理解和稳定性的把控。框架可以换但“把变化收敛到一处、把频繁操作抽象成通用方法、把失败信息做到一目了然”这套思路放在Web端、App端还是接口测试里都通用。如果你准备重构自己的UI自动化不妨先从基类和PageObject入手把最脏最乱的定位和等待先收拢起来剩下的会顺畅很多。本文还有配套的精品资源点击获取