ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

周三-自动愈合稳定层-工程化实战02

周三-自动愈合稳定层-工程化实战02 别再写 try/except 了——给浏览器自动化加一层自动愈合的稳定层「工程化实战」栏目 第 02 篇与周一《从 IP 到 Browser Context》的关系周一讲的是环境隔离L1/L2/L3 指纹这篇讲的是脚本自身的健壮性——环境隔离好了脚本本身还是会因为元素抖动、加载时序、偶发超时而挂。这篇给的是架构级答案。导读如果你的浏览器自动化脚本里到处都是这样的代码try:page.click(#submit)except:try:page.click(.btn-primary)except:try:page.click(text提交)except:page.screenshot(patherror.png)raise是时候停下来想想了。这不是工程这是用 try/except 堆出来的补丁。每加一个 except 就多一层脆弱性每多一个嵌套就多一个不知道为什么挂了的盲区。本文给出一个架构级方案——自动愈合层让脚本在元素抖动、加载时序偏移、偶发超时的时候自己修复自己而不是直接报错。三个组件Selector 重试、状态 fallback、截图证据链。一套 BasePage 模板 pytest 集成看完直接搬进项目。关键词Playwright 自动愈合、自动化测试稳定性、Selector 重试策略、BasePage 模板、pytest 浏览器自动化、截图证据链、元素定位失败重试、自动化测试架构一、问题拆解脚本为什么会偶发失败在讲方案之前先把偶发失败拆成三类。不同类型的失败修法完全不同失败类型典型表现根因正确修法A. Selector 抖动同一个按钮有时#submit能点有时不能页面 A/B 测试、动态 ID、多版本共存Selector 重试多 selector fallbackB. 时序偏移本地通过、CI 失败白天通过、晚上失败资源加载速度波动、网络抖动状态 fallback等状态而非等时间C. 偶发超时跑 100 次挂 1~2 次每次挂在不同位置不可控的外部因素CDN 抽风、DNS 解析慢截图证据链 自动重跑try/except 的问题在于它把这三类失败混为一谈统一用抓异常 → 换 selector → 再试来兜底既不区分原因也不记录现场。出了问题回头看日志只有一句TimeoutError啥也分析不了。二、什么是自动愈合层自动愈合的核心思想脚本遇到问题时先尝试自己修复修复失败才报错——并且报错时带上完整的现场证据。三个组件各管一类失败┌─────────────────────────────────────────┐ │ 自动愈合层 (AutoHeal) │ │ │ │ ┌──────────────┐ ┌────────────────┐ │ │ │ Selector 重试 │ │ 状态 Fallback │ │ │ │ (解决 A 类) │ │ (解决 B 类) │ │ │ └──────┬───────┘ └───────┬────────┘ │ │ │ │ │ │ └────────┬─────────┘ │ │ │ │ │ ┌────────▼─────────┐ │ │ │ 截图证据链 │ │ │ │ (解决 C 类 排障) │ │ │ └──────────────────┘ │ └─────────────────────────────────────────┘Selector 重试一个元素配多个 selector第一个不行自动换第二个状态 fallback不靠time.sleep()等时间而是等页面达到某个可观测状态截图证据链每次操作前后自动截图失败时自动保存完整的操作序列三、代码实现代码 1Selector 策略管理器先定义一个 Selector 策略——每个元素配多个定位方式按优先级排序fromdataclassesimportdataclass,fieldfromtypingimportListfromplaywright.sync_apiimportPage,LocatordataclassclassSelectorStrategy:元素的多元 selector 策略name:str# 元素名称用于日志selectors:List[str]# 多个 selector按优先级排序description:str# 人可读的描述deffind(self,page:Page,timeout:int10000)-Locator:按优先级尝试每个 selector返回第一个找到的last_errorNonefori,selinenumerate(self.selectors):try:locatorpage.locator(sel)# 等元素出现但不等它可交互那是上层的事locator.wait_for(statevisible,timeouttimeout)returnlocatorexceptExceptionase:last_erroreprint(f[SelectorStrategy] {self.name} fselector #{i1}{sel} 失败:{e})raiseRuntimeError(f元素 {self.name} 所有 selector 均失败。f尝试了:{self.selectors}。最后错误:{last_error})# --- 定义页面的元素策略 ---SUBMIT_BTNSelectorStrategy(name提交按钮,selectors[#submit-btn,# 最稳IDbutton[typesubmit],# 次选属性.btn-primary text提交,# 兜底文本rolebutton[name提交],# 最后无障碍角色],description购物车页面的提交按钮4 种定位方式)关键设计selector 从最精确的ID到最模糊的角色/文本按优先级降级。这样即使页面改版改了 ID脚本也能用兜底 selector 继续跑。代码 2BasePage 基类含自愈 click / fill / waitimportosfromdatetimeimportdatetimefrompathlibimportPathfromplaywright.sync_apiimportPage,LocatorclassBasePage:所有页面对象的基类内置自动愈合能力def__init__(self,page:Page,screenshot_dir:strscreenshots):self.pagepage self.screenshot_dirPath(screenshot_dir)self.screenshot_dir.mkdir(parentsTrue,exist_okTrue)self._action_count0# 操作计数用于截图命名defheal_click(self,strategy:SelectorStrategy,timeout:int10000)-None:自愈点击多 selector 降级 前后截图self._before_action(strategy,click)locatorstrategy.find(self.page,timeouttimeout)# 等元素可交互不只是 visible还要 enabledlocator.wait_for(statevisible,timeouttimeout)# 检查元素状态ifnotlocator.is_enabled():raiseRuntimeError(f元素 {strategy.name} 可见但不可点击disabled)locator.click()self._after_action(strategy,click)defheal_fill(self,strategy:SelectorStrategy,text:str,timeout:int10000)-None:自愈填充多 selector 降级 前后截图self._before_action(strategy,ffill({text}))locatorstrategy.find(self.page,timeouttimeout)locator.wait_for(statevisible,timeouttimeout)# 先清空再填避免追加locator.fill()locator.fill(text)self._after_action(strategy,ffill({text}))defwait_for_state(self,strategy:SelectorStrategy,state:strvisible,timeout:int15000)-None: 状态 fallback等元素达到某个状态而不是 sleep 固定时间。 state 可选: attached | detached | visible | hidden print(f[BasePage] 等待 {strategy.name} 状态{state})locatorstrategy.find(self.page,timeouttimeout)locator.wait_for(statestate,timeouttimeout)# ---- 截图证据链 ----def_before_action(self,strategy:SelectorStrategy,action:str)-None:操作前截图self._action_count1tsdatetime.now().strftime(%H%M%S_%f)pathself.screenshot_dir/f{self._action_count:03d}_{strategy.name}_{action}_BEFORE_{ts}.pngtry:self.page.screenshot(pathstr(path))print(f[证据链] 操作前截图:{path})exceptExceptionase:print(f[证据链] 截图失败不影响主流程:{e})def_after_action(self,strategy:SelectorStrategy,action:str)-None:操作后截图tsdatetime.now().strftime(%H%M%S_%f)pathself.screenshot_dir/f{self._action_count:03d}_{strategy.name}_{action}_AFTER_{ts}.pngtry:self.page.screenshot(pathstr(path))print(f[证据链] 操作后截图:{path})exceptExceptionase:print(f[证据链] 截图失败不影响主流程:{e})关键设计heal_click/heal_fill接收的是SelectorStrategy而不是字符串——强制你为每个元素定义多元 selector从根源上消除selector 写死的脆弱性操作前后各截一张图形成证据链——失败时可以按序号回放wait_for_state等的是状态visible / detached不是时间——解决 B 类时序偏移问题截图失败不影响主流程try/except只用在截图上不用在业务逻辑上代码 3pytest 集成 自动重跑importpytestfromplaywright.sync_apiimportsync_playwrightfrompages.base_pageimportBasePagefrompages.selectorsimportSUBMIT_BTN,USERNAME_INPUT,PASSWORD_INPUTclassTestLoginPage(BasePage):登录页测试——继承 BasePage自带自愈能力pytest.fixture(autouseTrue)defsetup_page(self):withsync_playwright()asp:browserp.chromium.launch(headlessTrue)# 周一那篇讲的用 new_context 隔离环境contextbrowser.new_context(viewport{width:1920,height:1080},localezh-CN,)self.pagecontext.new_page()# BasePage 初始化带截图目录super().__init__(self.page,screenshot_dirscreenshots/login_test)self.page.goto(https://example.com/login)yieldcontext.close()browser.close()deftest_login_success(self):用自愈方法跑登录流程# 填用户名——即使页面改了 ID兜底 selector 也能找到self.heal_fill(USERNAME_INPUT,test_user)self.heal_fill(PASSWORD_INPUT,test_pass)# 等提交按钮可交互状态 fallback不 sleepself.wait_for_state(SUBMIT_BTN,statevisible)self.heal_click(SUBMIT_BTN)# 验证登录成功——同样用多元 selectorself.wait_for_state(SelectorStrategy(name仪表盘,selectors[.dashboard,#main-content,text欢迎],),statevisible,timeout20000,)# ---- pytest 自动重跑配置conftest.py----# 在项目根目录的 conftest.py 里加## import pytest## pytest.fixture(scopesession)# def rerun_config():# return {reruns: 2, reruns_delay: 3}## # 命令行运行时加 --reruns 2 --reruns-delay 3# # 需要 pip install pytest-rerunfailures与 pytest 集成的好处pytest-rerunfailures插件提供自动重跑——C 类偶发超时自动重试 2 次不需要在代码里写 retry 逻辑截图证据链和 pytest 的tmp_path/ HTML 报告天然兼容每个测试类继承BasePage所有自愈能力开箱即用四、效果对比指标修复前try/except 堆叠修复后自动愈合层Selector 变更手动改代码多 selector 自动降级零改动偶发超时报错 → 人工重跑pytest 自动重跑 2 次排障效率日志只有TimeoutError操作前后截图 selector 尝试日志代码量每个用例重复 try/exceptBasePage 统一封装用例只调heal_click100 次通过率~92%~99%2 次重跑兜底五、与周一文章的串联周一《从 IP 到 Browser Context》讲的是环境隔离——让脚本跑在干净的 Browser Context 里不被其他任务干扰。这篇讲的是脚本自愈——让脚本在环境已经隔离好的前提下依然能应对元素抖动和时序偏移。两者解决的是不同层次的问题第一层周一环境隔离 → 让脚本跑在干净环境里 第二层本周脚本自愈 → 让脚本在干净环境里也不容易挂 第三层周五预告环境指纹锁定 → 让 CI 环境和本地环境一致如果你只看了周一那篇这篇的增量是环境隔离解决的是外部干扰自愈层解决的是脚本自身的脆弱性——两者叠加才是工程化的稳定方案。六、小结三件事记住就够别再裸写 try/except——用SelectorStrategy为每个元素配多元 selector自动降级别再 sleep——用wait_for_state等状态不等活动时间别再裸跑——用pytest-rerunfailures自动重跑 截图证据链排障把这三件事做到你的浏览器自动化脚本通过率从 92% 提到 99%不是什么难事。「工程化实战」栏目 第 02 篇关注「浏览器自动化实战」每周一到两篇从踩坑升级到工程化的实战内容。下一篇预告《Playwright vs Selenium 4 vs Puppeteer在 2026 年浏览器自动化该选谁》周四发工具横评栏目如果这篇对你有用点个「在看」和「收藏」——这是本栏目的主力留粉篇你的收藏是我判断内容方向的重要信号。本文同步发布于公众号「浏览器自动化实战」、知乎、CSDN。代码均已在本地 Playwright 1.40 (Python) pytest 8.x 跑通验证。依赖pip install playwright pytest pytest-rerunfailures
返回列表