1. 项目概述为什么异常处理是自动化测试的“安全带”做自动化测试尤其是UI自动化最怕什么不是脚本写不出来而是脚本跑着跑着就“死”了。页面元素加载慢了一秒、网络突然抖动、弹窗不期而至任何一个意外都可能导致精心编写的测试用例中断留下一堆“红色”的失败记录却难以定位根因。这就是为什么异常处理技巧对于任何自动化测试框架尤其是像Playwright这样功能强大的现代工具其重要性不亚于测试逻辑本身。它就像是给测试脚本系上的一条“安全带”确保在颠簸的测试旅程中即使遇到意外也能安全地记录现场、尝试恢复而不是直接“车毁人亡”。Playwright以其跨浏览器、速度快、API设计优雅而著称但很多新手甚至是有经验的测试开发者往往只关注其强大的定位和操作能力而忽略了异常处理的构建。结果就是测试脚本在理想环境下运行良好一旦放到真实的、充满不确定性的CI/CD流水线或不同环境的测试机上就变得脆弱不堪。异常处理的核心目标是让测试具备“韧性”Resilience。它不仅仅是捕获一个错误然后打印出来更是关于如何优雅地处理失败、如何收集足够的诊断信息、以及如何在可能的情况下继续执行后续测试从而最大化每次测试运行的产出和价值。2. Playwright异常处理的核心思想与架构2.1 从“防御式编程”到“韧性测试”在Playwright的上下文中谈异常处理首先要转变一个观念不要指望测试环境是完美和稳定的。相反我们应该假设任何操作都可能失败——元素可能找不到、点击可能无效、页面可能超时。基于这种“悲观”假设进行编程就是防御式编程。而Playwright的异常处理机制正是帮助我们实践这种思想的有力工具。Playwright的异常大致可以分为几类超时异常TimeoutError这是最常见的一类。当page.waitForSelector、page.click等操作在指定时间内未完成时抛出。原因可能是元素确实不存在、网络慢、或是页面JS阻塞。定位异常Locator Error当使用page.locator()找到一个不唯一的元素或试图对一个不可操作的元素如被遮挡、不可见执行操作时发生。导航异常Navigation Error页面跳转失败例如遇到404、网络断开、SSL证书错误等。脚本执行异常Evaluation Error在page.evaluate()中执行的JavaScript代码本身抛出了错误。浏览器上下文异常BrowserContext Error浏览器实例意外崩溃或断开连接。一个健壮的测试框架需要针对这些不同类型的异常设计分层的捕获和处理策略。2.2 Playwright的内置等待与自动重试机制很多人把异常处理和等待机制分开看其实在Playwright里它们是紧密结合的第一道防线。Playwright的Locator API内置了智能等待和自动重试这是它优于Selenium等传统工具的一大特点。当你写下await page.locator(button#submit).click()时Playwright内部会执行一系列操作自动等待该元素出现在DOM中并变得可见、可交互即通过“可操作性”检查。等待直到元素处于稳定状态例如不再有动画效果。滚动元素到视图中。尝试点击。这个过程本身已经包含了“异常预防”。如果元素在默认的超时时间通常是30秒内没有满足条件Playwright才会抛出异常。这意味着很多因页面加载速度导致的间歇性失败已经被这套内置机制消化掉了。你的首要任务不是急着去写try-catch而是合理利用和配置这些内置行为。例如对于已知加载较慢的元素你应该优先考虑增加这个特定操作的超时时间而不是全局提高超时# 优先方案针对特定操作增加超时 await page.locator(div.lazy-loaded-content).wait_for(timeout60000) # 等待60秒 await page.locator(div.lazy-loaded-content).click(timeout60000) # 次选方案修改全局超时谨慎使用 page.set_default_timeout(60000)实操心得不要滥用全局超时。过长的全局超时会让脚本在真正失败时等待过久拖慢测试套件的整体执行速度。将超时配置在“操作”级别或“Locator”级别是更精细和推荐的做法。3. 结构化异常处理Try-Catch的进阶用法当内置等待无法解决问题时我们就需要主动捕获和处理异常。Python的try-except和JavaScript/TypeScript的try-catch是基本工具但用在Playwright中需要一些技巧。3.1 精准捕获特定异常盲目地捕获所有异常except Exception或catch (error)会掩盖真正的问题。你应该尽可能捕获具体的Playwright异常类型。from playwright.sync_api import TimeoutError, Error as PlaywrightError try: await page.locator(textConfirm).click() except TimeoutError: # 处理元素未找到或不可点击的情况 print(确认按钮未在预期时间内出现可能页面状态有误。) # 可以在这里附加截图等诊断操作 await page.screenshot(pathtimeout_error.png) # 然后决定是让测试失败还是执行备用方案 raise except PlaywrightError as e: # 捕获其他Playwright相关错误 print(f发生Playwright错误: {e}) raise在这个例子中我们区分了TimeoutError和其他PlaywrightError。对于超时我们可能想截图记录当前页面状态这对于后续调试至关重要。对于其他错误我们可能直接抛出让测试框架将其标记为失败。3.2 异常处理中的资源清理与状态重置这是关键但常被忽略的一点。当异常发生时测试所处的状态可能是不确定的。在except块或finally块中进行清理是保证后续测试不受影响的关键。import pytest from playwright.sync_api import Page def test_purchase_flow(page: Page): cart_cleanup_needed False try: await page.goto(/products/abc) await page.locator(button.add-to-cart).click() cart_cleanup_needed True # 标记购物车已有商品 # ... 其他购买步骤 await page.locator(button#checkout).click() except Exception as e: # 无论成功与否最后都要确保清理测试数据 print(f测试失败: {e}) if cart_cleanup_needed: # 跳转到购物车页面清空商品这是一个状态重置操作 await page.goto(/cart) await page.locator(button.empty-cart).click() raise # 重新抛出异常让测试框架知道这个用例失败了 finally: # finally块中的代码无论是否发生异常都会执行 # 适合做最兜底的清理比如关闭非必要的弹窗 pass这个模式确保了即使购买流程失败也不会留下一个脏的购物车状态去影响下一个测试用例。在UI自动化中管理测试状态比管理代码状态更重要。4. 利用Playwright Hook与事件监听进行全局处理对于某些类型的异常在每个操作里都写try-catch太繁琐了。Playwright提供了BrowserContext和Page级别的事件监听器允许你设置全局的异常处理或日志记录逻辑。4.1 监听页面崩溃与错误# 在创建浏览器上下文后设置监听 context await browser.new_context() # 监听页面崩溃浏览器进程意外退出 context.on(crash, lambda page: print(f页面崩溃: {page.url})) # 监听页面错误JavaScript未捕获的异常 context.on(pageerror, lambda error: print(f页面JS错误: {error})) # 监听请求失败如404网络错误 context.on(requestfailed, lambda request: print(f请求失败: {request.url} - {request.failure().error_text if request.failure() else Unknown}) )通过全局监听requestfailed你可以轻松发现因为资源加载失败如一个关键的CSS或JS文件404导致的页面样式错乱或功能失效而这种问题通过元素定位超时是很难直接定位根源的。4.2 创建自定义Fixture以Pytest为例如果你使用Pytest结合Playwright的Pytest插件可以创建功能强大的自定义Fixture将异常处理、日志和截图能力封装起来。# conftest.py import pytest from playwright.sync_api import Page, TimeoutError import datetime pytest.fixture def resilient_page(page: Page): 提供一个具备增强异常处理能力的page fixture original_goto page.goto original_click page.click async def goto_with_screenshot(url, **kwargs): try: return await original_goto(url, **kwargs) except Exception as e: timestamp datetime.datetime.now().strftime(%Y%m%d_%H%M%S) await page.screenshot(pathf./screenshots/nav_error_{timestamp}.png, full_pageTrue) print(f导航到 {url} 失败截图已保存。错误: {e}) raise async def click_with_retry(selector, **kwargs): max_retries 2 for attempt in range(max_retries): try: return await original_click(selector, **kwargs) except TimeoutError: if attempt max_retries - 1: timestamp datetime.datetime.now().strftime(%Y%m%d_%H%M%S) await page.screenshot(pathf./screenshots/click_timeout_{timestamp}.png) raise print(f点击 {selector} 超时第{attempt1}次重试...) await page.wait_for_timeout(1000) # 等待1秒后重试 page.goto goto_with_screenshot page.click click_with_retry return page然后在你的测试用例中使用resilient_page代替普通的pagedef test_example(resilient_page): await resilient_page.goto(https://example.com) # 自动附带截图功能 await resilient_page.click(button) # 自动附带重试逻辑这个Fixture演示了两个高级技巧一是通过包装原生方法为导航失败自动添加截图二是为点击操作实现了简单的重试逻辑。这是一种非侵入式的增强能让你的测试用例代码保持简洁同时获得强大的容错能力。5. 断言失败与软断言让测试报告更清晰Playwright Test或配合其他测试框架如Pytest自带的断言库在失败时会抛出异常导致测试立即停止。但有时你可能希望收集一个测试用例中的所有验证点结果最后再统一报告而不是遇到第一个失败就退出。这就是“软断言”的概念。5.1 实现简单的软断言收集器class SoftAssert: def __init__(self): self.errors [] async def assert_equal(self, actual, expected, message): try: assert actual expected, f{message} | 预期: {expected}, 实际: {actual} except AssertionError as e: self.errors.append(str(e)) # 可以在这里也截图记录哪个断言失败了 print(f软断言失败: {e}) def assert_all(self): if self.errors: failure_message \n.join(self.errors) raise AssertionError(f测试包含以下断言失败\n{failure_message}) # 在测试中使用 def test_user_profile(soft_assert: SoftAssert, page: Page): await page.goto(/profile) name await page.locator(#user-name).text_content() await soft_assert.assert_equal(name, 测试用户, 用户名显示不正确) email await page.locator(#user-email).text_content() await soft_assert.assert_equal(email, testexample.com, 邮箱显示不正确) # ... 更多断言 # 所有检查完成后统一抛出所有错误 soft_assert.assert_all()使用软断言即使中间某个检查点如邮箱显示失败测试也会继续执行完所有后续检查如检查头像、权限等最后在assert_all()处一次性报告所有问题。这对于验收测试或端到端测试非常有用能提供一份完整的“体检报告”而不是只告诉你第一个毛病。注意事项软断言会改变测试的执行流程可能使调试变得稍微复杂因为测试不是在第一个问题点停住。因此它更适合用于那些失败点相对独立、且你希望获得完整失败场景的用例。对于关键路径上的致命错误仍应使用硬断言。6. 与测试报告框架集成Allure中的失败分析与视频异常处理的最终目的是为了更好的分析和调试。与Allure、HTMLTestRunner等报告框架集成能将异常发生时的上下文截图、视频、日志生动地展现出来。6.1 配置Playwright录制测试视频在Playwright配置文件中如playwright.config.ts或pytest.ini中配置可以非常方便地开启每次测试的视频录制// playwright.config.ts import { defineConfig, devices } from playwright/test; export default defineConfig({ use: { video: retain-on-failure, // 仅在失败时保留视频 // 或 ‘on’ 每次都录制‘off’ 关闭 }, // ... 其他配置 });当测试因异常而失败时Playwright会自动将录制好的视频文件保存下来。retain-on-failure是一个很实用的设置它避免了成功用例产生大量无用视频文件节省存储空间。6.2 在Allure报告中附加截图与视频你需要将Playwright运行过程中捕获的截图和视频作为附件添加到Allure报告中。这通常需要在测试的teardown阶段或异常处理逻辑中完成。# pytest allure-pytest 示例 import allure from playwright.sync_api import Page def test_example(page: Page): try: # ... 测试步骤 await page.click(button.that-may-fail) except Exception as e: # 1. 截图并附加到Allure screenshot await page.screenshot(full_pageTrue) allure.attach(screenshot, name失败截图, attachment_typeallure.attachment_type.PNG) # 2. 获取视频文件路径并附加需要知道视频保存位置 # 假设你知道视频保存在 test-results/ 目录下 video_path page.video.path() if page.video else None if video_path and os.path.exists(video_path): allure.attach.file(video_path, name失败回放视频, attachment_typeallure.attachment_type.WEBM) # 3. 附加页面源代码有时比截图更有用 html await page.content() allure.attach(html, name失败时页面HTML, attachment_typeallure.attachment_type.HTML) raise # 重新抛出异常标记测试失败这样当你在Allure报告中查看这个失败的测试用例时可以直接在“附件”区域看到失败瞬间的截图、回放整个操作过程的视频甚至查看当时的页面HTML源码极大提升了定位问题的效率。实操心得视频文件通常较大可以考虑在CI/CD流水线中将视频上传到持久的对象存储如S3、MinIO然后在Allure报告中链接到该地址而不是直接嵌入报告文件内以免报告体积过大。7. 常见疑难场景与排查技巧实录即使掌握了上面的技巧在实际项目中还是会遇到一些棘手的异常场景。下面记录几个典型案例和我的排查思路。7.1 场景一元素“忽隐忽现”定位不稳定现象脚本有时能定位到元素并成功操作有时却报TimeoutError。在慢速网络或性能较差的机器上尤其明显。排查与解决检查选择器首先确认你的选择器是否稳定。避免使用基于索引如:nth-child(3)或包含动态文本如text订单12345的选择器。优先使用># 等待元素可见且稳定 await page.locator(button.submit).wait_for(statevisible, timeout10000) # 或者等待某个特定条件满足如表单验证通过 await page.wait_for_function(document.querySelector(.is-valid) ! null)重试机制对于非关键性操作或已知不稳定的元素实现一个简单的重试循环。async def click_with_retry(locator, max_attempts3): for attempt in range(max_attempts): try: await locator.click(timeout5000) # 每次尝试给5秒超时 return except TimeoutError: if attempt max_attempts - 1: raise print(f点击尝试 {attempt1} 失败等待后重试...) await page.wait_for_timeout(1000)7.2 场景二页面弹窗Modal/Dialog干扰现象脚本运行时突然弹出广告、Cookie同意框或系统通知遮挡了目标元素。排查与解决主动关闭弹窗在测试开始或关键步骤前先尝试查找并关闭常见的弹窗。# 尝试关闭可能的弹窗 close_buttons page.locator(button.close, .modal-close, [aria-labelClose]) if await close_buttons.count() 0: await close_buttons.first.click()监听Dialog事件对于浏览器原生alert,confirm,prompt必须在它们出现前设置监听否则会阻塞Playwright脚本。page.on(dialog, lambda dialog: dialog.accept()) # 自动接受所有弹窗 # 或者更精细地处理 page.on(dialog, lambda dialog: print(f弹窗消息: {dialog.message}) if 确认删除 in dialog.message: dialog.dismiss() # 取消 else: dialog.accept() # 接受 )注意page.on(dialog)监听器必须在可能触发弹窗的操作之前设置。7.3 场景三网络请求失败导致后续状态错误现象测试失败但截图显示页面是白的或者样式错乱控制台看到net::ERR_CONNECTION_之类的错误。排查与解决启用请求/响应日志在调试时可以拦截和记录所有网络活动。page.on(request, lambda request: print(f {request.method} {request.url})) page.on(response, lambda response: if not response.ok: print(f {response.status} {response.url}) )模拟弱网或离线在CI环境中网络问题可能被放大。使用Playwright的context.setOffline(True)可以模拟离线状态测试应用的降级处理。用browser.new_context的viewport,userAgent以及模拟网络条件slow3G来创建更真实的测试环境。关键请求断言对于重要的XHR或Fetch请求可以等待其完成后再进行下一步操作确保数据已加载。# 等待一个特定的API请求完成 async with page.expect_response(**/api/user/profile) as response_info: await page.click(#load-profile) response await response_info.value assert response.ok user_data await response.json()7.4 场景四在CI/CD中因环境差异导致的失败现象脚本在本地开发机运行完美一到Jenkins/GitHub Actions上就跑失败。排查与解决使用官方Docker镜像Playwright提供了包含所有依赖的Docker镜像mcr.microsoft.com/playwright。在CI中使用它能保证浏览器版本、系统库与本地一致。确保有头模式在无头模式下headlessTrue某些渲染或动画行为可能与有头模式不同。如果测试在有头模式下通过而无头模式下失败尝试增加超时时间因为无头模式有时稍慢。使用headlessFalse运行CI测试需要配置虚拟显示服务器如Xvfb。** artifacts产物收集**确保CI配置能正确收集并归档测试失败时生成的截图、视频和追踪文件。这是远程调试的生命线。资源清理CI环境通常是共享的上一个测试留下的数据可能影响下一个。务必在每个测试用例或套件的setUp/tearDown阶段做好彻底的清理如清理数据库测试数据、浏览器Cookies、LocalStorage。处理这些疑难杂症的过程让我深刻体会到UI自动化测试的稳定性三分靠脚本七分靠对异常和边界的深刻理解与妥善处理。把每一次失败都当作完善测试“韧性”的机会仔细分析日志、截图和视频不断优化你的选择器、等待策略和错误恢复逻辑你的自动化测试套件才会真正成为值得信赖的质量守护者而不是一个需要频繁维护的“瓷娃娃”。