Python Playwright自动化测试:同步与异步模式深度解析与实战指南
1. 项目概述为什么我们需要深入理解Playwright的同步与异步如果你正在用Python做Web自动化测试或者正准备从Selenium切换到更现代的框架那么Playwright绝对是一个绕不开的名字。它由微软出品支持Chromium、Firefox和WebKit三大浏览器引擎号称能解决Selenium的诸多痛点比如更稳定的元素定位、更快的执行速度以及原生的等待机制。但当你真正上手尤其是从官方文档或一些快速入门教程里看到async/await关键字时很多朋友可能会心头一紧这异步编程是不是很复杂我能不能就用传统的同步写法这正是我写这篇实战解析的初衷。在过去一年的项目里我带领团队将核心的Web自动化测试从Selenium全面迁移到了Playwright期间深度使用了它的两种模式同步API和异步API。我发现网上很多资料要么只讲同步要么浅尝辄止地提一下异步很少有人把这两种模式放在一起从设计原理、性能差异、适用场景到实际代码的坑进行透彻的对比和剖析。这导致很多团队在选型和实施时走了弯路要么用同步模式处理高并发场景力不从心要么在简单的线性脚本里强行上异步把代码写得复杂无比。简单来说这个项目标题“PythonPlaywright自动化测试实战同步与异步模式深度解析”就是要解决一个核心问题在面对不同的自动化测试需求时我们该如何明智地选择同步或异步模式并写出高效、健壮的代码这不仅关乎脚本是否能跑起来更关乎测试套件的执行效率、资源利用率和长期维护成本。无论你是刚接触Playwright的新手还是已经用它写过一些脚本但想更进一步的测试开发工程师这篇文章都将通过大量的对比实例和底层原理分析帮你彻底理清思路。2. 核心概念与模式选型背后的逻辑在深入代码之前我们必须先建立正确的认知。Playwright的同步和异步并非简单的“两种写法”而是其底层架构设计所自然呈现的两种使用方式。理解这一点是做出正确技术选型的前提。2.1 同步模式简单直接的线性思维同步模式是大多数从Selenium转过来的工程师最熟悉的方式。它的代码执行流程是线性的、阻塞的。当你写下page.click(“button”)这行代码时程序会停下来等待这个点击操作完全执行完毕包括等待元素可点击、执行点击、等待可能的页面导航完成才会继续执行下一行。它的核心优势在于符合直觉易于理解和调试。你的脚本顺序就是执行顺序阅读起来就像在看一个操作清单。对于大多数中小型的、线性的业务流程自动化例如登录-填写表单-提交-验证结果同步模式完全够用且开发效率很高。Playwright在同步模式下通过内部的事件循环和智能等待机制隐藏了大部分异步的复杂性让你几乎感觉不到自己在操作一个异步驱动的浏览器。注意这里的“同步”是相对于你的Python代码流而言。实际上Playwright核心与浏览器的通信通过WebSocket仍然是异步的。同步API只是在这个异步核心之上封装了一个阻塞式的调用层让你的代码可以“假装”在同步执行。这是Playwright设计巧妙的地方也是其API稳定性的来源。2.2 异步模式拥抱并发的性能利器异步模式则直接暴露了Playwright的异步本质。它要求你使用async/await语法。当你调用await page.click(“button”)时这行代码会“挂起”当前协程将控制权交还给事件循环事件循环可以在此期间去处理其他任务比如另一个标签页的加载、网络请求的监听等。当点击操作完成后事件循环再回来恢复这个协程的执行。它的核心价值在于高效处理I/O密集型任务和实现并发。Web自动化测试的绝大部分时间都在等待等待页面加载、等待元素出现、等待网络响应。在同步模式下一个测试用例在等待时整个线程是被阻塞的什么也干不了。而在异步模式下一个用例在等待时事件循环可以切换到另一个已经就绪的用例去执行操作。这意味着你可以在单线程内并发执行多个测试流极大地提升测试执行效率尤其是在需要并行操作多个浏览器上下文Context或页面Page的场景下。2.3 如何选择一张决策表帮你理清面对一个具体项目你该如何选择我总结了一个简单的决策矩阵基于几个关键维度考量维度推荐使用同步模式推荐使用异步模式团队技术栈团队不熟悉asyncioPython版本较低3.5团队已熟悉异步编程或愿意学习测试场景复杂度简单的、线性的端到端E2E测试流程复杂的、需要多页面交互、长轮询、WebSocket测试执行规模与性能测试用例数较少执行环境资源充足对执行时间不敏感测试套件庞大需要在有限资源如CI/CD服务器内快速完成追求高并发执行框架集成与pytest等框架以传统方式集成追求简单配置希望深度集成利用pytest-asyncio等插件发挥异步优势主要优势代码简单学习曲线低调试直观资源利用率高执行速度快适合复杂异步交互根据我的经验一个实用的建议是新手或项目初期从同步模式开始快速实现核心业务流程的自动化。当测试套件增长执行时间成为瓶颈或者需要测试复杂的前端交互时再考虑将部分或全部代码迁移到异步模式。Playwright的两种模式API高度一致迁移成本相对较低。3. 环境搭建与两种模式的初始化对比理论说再多不如动手跑一行代码。我们首先来看看在项目伊始同步和异步模式在环境搭建和初始化上就有哪些不同。这些不同奠定了整个代码的基调。3.1 基础环境准备无论同步异步前提是安装Playwright。我强烈建议使用虚拟环境如venv或conda来管理依赖避免污染全局环境。# 1. 创建并激活虚拟环境以venv为例 python -m venv playwright-env source playwright-env/bin/activate # Linux/macOS # playwright-env\Scripts\activate # Windows # 2. 安装playwright的python包 pip install playwright # 3. 安装浏览器驱动Chromium, Firefox, WebKit playwright install第三步playwright install非常重要它会下载所需的浏览器二进制文件到本地缓存。这些浏览器是专门为自动化定制的版本比你自己安装的普通浏览器更稳定。3.2 同步模式下的“标准开局”同步模式的脚本结构非常传统导入sync_playwright然后在with语句块内执行所有操作。# sync_demo.py from playwright.sync_api import sync_playwright def run_sync(): # 使用 sync_playwright() 作为上下文管理器启动Playwright with sync_playwright() as p: # 启动一个Chromium浏览器实例headlessFalse表示有界面方便调试 browser p.chromium.launch(headlessFalse) # 创建一个新的浏览器上下文类似于一个独立的会话可隔离cookies、localStorage等 context browser.new_context() # 在新上下文中打开一个页面 page context.new_page() # 线性执行操作导航、操作、断言 page.goto(https://example.com) page.screenshot(pathsync-example.png) title page.title() print(f页面标题同步: {title}) assert Example in title # 操作结束后关闭上下文和浏览器 context.close() browser.close() if __name__ __main__: run_sync()同步模式的心得with sync_playwright() as p:这行代码是同步模式的灵魂。这个上下文管理器负责了Playwright整个生命周期的启动和清理包括背后事件循环的创建和销毁。你几乎不需要关心asyncio的任何细节就像使用一个普通的库一样。3.3 异步模式下的“协程启动”异步模式则需要你显式地处理事件循环。通常我们会定义一个async函数协程来包裹主要逻辑。# async_demo.py import asyncio from playwright.async_api import async_playwright # 主逻辑定义为一个异步函数 async def run_async(): # 使用 async_playwright() 作为异步上下文管理器 async with async_playwright() as p: # 异步启动浏览器 browser await p.chromium.launch(headlessFalse) # 异步创建上下文 context await browser.new_context() # 异步创建页面 page await context.new_page() # 使用await关键字执行异步操作 await page.goto(https://example.com) await page.screenshot(pathasync-example.png) title await page.title() print(f页面标题异步: {title}) assert Example in title # 异步关闭 await context.close() await browser.close() # 获取或创建事件循环并运行主协程 if __name__ __main__: asyncio.run(run_async())异步模式的关键点导入差异从playwright.async_api导入而不是sync_api。async/await无处不在几乎所有可能涉及I/O等待的方法调用前都需要加上await。忘记加await是最常见的错误之一会导致返回一个协程对象Coroutine而非实际结果引发难以理解的错误。启动方式使用asyncio.run()来运行最顶层的异步函数。这是Python 3.7之后推荐的方式它负责创建、运行事件循环并最终关闭。实操心得在异步脚本中我习惯将所有与Playwright交互的操作都集中写在一个async函数里这样逻辑清晰。而asyncio.run()就像是这个异步世界的“总开关”。4. 核心API使用对比从导航到断言初始化之后日常使用中绝大部分API在同步和异步模式下名称和功能都是一致的只是调用方式有别。我们通过几个最常见的使用场景来对比。4.1 页面导航与等待导航是自动化测试的第一步也是最容易出问题的一步因为页面加载时间不确定。同步模式# 同步导航默认会等待到 load 事件触发 page.goto(https://my-app.com/login) # 也可以显式等待到某个特定状态比如网络空闲 page.goto(https://my-app.com/dashboard, wait_untilnetworkidle)异步模式# 异步导航同样需要await await page.goto(https://my-app.com/login) # 带参数的等待 await page.goto(https://my-app.com/dashboard, wait_untilnetworkidle)参数解析与选择wait_until参数至关重要它决定了goto方法何时算“完成”。load默认等待load事件触发。对于简单页面够用。domcontentloaded等待DOMContentLoaded事件触发此时HTML已解析完成但样式表、图片等可能还在加载。速度比load快。networkidle等待网络连接数在至少500毫秒内不超过0个即没有正在进行的网络请求。这对于等待SPA单页应用通过Ajax加载完动态内容非常有效。commit当接收到网络响应并开始加载文档时即认为完成最快但最不稳定。避坑指南对于现代前端框架如React, Vue, Angular构建的应用强烈建议使用wait_untilnetworkidle或结合page.wait_for_load_state(networkidle)。我遇到过太多用例在load状态下就进行下一步操作结果元素还没被JavaScript渲染出来导致定位失败。这是从Selenium迁移过来后需要转变的一个关键思维——Playwright给了你更精细的等待控制权。4.2 元素定位与交互定位和操作元素是自动化的核心。Playwright提供了丰富且稳定的定位器Locator。同步模式# 通过文本定位按钮并点击 page.click(button:has-text(Submit)) # 通过CSS选择器定位输入框并填充文本 page.fill(#username, myuser) # 先获取一个Locator对象再进行一系列操作推荐方式 submit_btn page.locator(button.submit-btn) submit_btn.click() submit_btn.is_enabled()异步模式# 异步点击 await page.click(button:has-text(Submit)) # 异步填充 await page.fill(#username, myuser) # Locator的操作也需要await submit_btn page.locator(button.submit-btn) await submit_btn.click() is_enabled await submit_btn.is_enabled()Locator模式的优势上面例子中page.locator()返回的是一个Locator对象它代表一个元素查询而不是立即执行操作。你可以多次使用这个对象并且Playwright内部会对这些操作进行智能的重试和等待比直接使用page.click(selector)更健壮。这是Playwright相比Selenium的一个巨大进步务必养成使用Locator的习惯。4.3 处理弹窗、对话框和下载处理浏览器弹窗如alert,confirm,prompt或监听下载两种模式在监听逻辑上类似但处理方式因异步特性而略有不同。同步模式使用on监听器# 同步模式下使用 page.on 注册事件监听器 with page.expect_download() as download_info: page.click(a#download-link) # 触发下载的点击操作 download download_info.value # 同步等待下载完成并保存到指定路径 download.save_as(/path/to/save/file.zip)异步模式使用异步上下文管理器或回调# 方法1使用异步上下文管理器推荐更清晰 async with page.expect_download() as download_info: await page.click(a#download-link) download await download_info.value await download.save_as(/path/to/save/file.zip) # 方法2使用回调函数适用于复杂事件流 def handle_dialog(dialog): print(f对话框文本: {dialog.message}) dialog.accept() # 或 dialog.dismiss() page.on(dialog, handle_dialog) # 监听所有对话框 await page.click(button.that-opens-dialog)注意事项page.expect_event()如expect_download,expect_popup是Playwright处理预期事件的利器。它返回一个上下文管理器会等待指定事件发生并将事件对象捕获。在同步代码中with块结束时会自动等待在异步代码中async with块结束时会用await等待。务必确保触发事件的操作如click发生在上下文管理器内部否则可能会错过事件。4.4 断言与验证测试离不开断言。Playwright推荐使用其内置的expect断言库它专为动态Web内容设计自带自动等待和丰富的匹配器。同步模式from playwright.sync_api import expect # 验证元素可见 expect(page.locator(.success-message)).to_be_visible() # 验证文本内容 expect(page.locator(h1)).to_have_text(Welcome Back) # 验证元素属性 expect(page.locator(input#email)).to_have_attribute(type, email)异步模式from playwright.async_api import expect # 异步断言同样需要await await expect(page.locator(.success-message)).to_be_visible() await expect(page.locator(h1)).to_have_text(Welcome Back) await expect(page.locator(input#email)).to_have_attribute(type, email)为什么用expect而不用assert传统的assert语句是立即执行的。如果元素因为加载慢还没出现断言会立刻失败。而expect断言内部包含了重试逻辑它会在超时时间可配置内不断检查条件是否满足这更符合Web自动化的实际情况能大幅减少因时机问题导致的“假失败”。5. 高级场景与并发实战当基础操作掌握后同步和异步模式的差异在高级和并发场景下会体现得淋漓尽致。这里我们探讨两个典型场景并发执行多个浏览器上下文以及在一个测试中同时控制多个标签页。5.1 场景一并发执行独立的测试用例假设我们需要同时运行三个独立的测试流程分别测试登录、搜索和浏览商品。在同步模式下你只能顺序执行总耗时是三者之和。同步模式顺序执行import time from playwright.sync_api import sync_playwright def test_login(): # ... 模拟登录流程 time.sleep(2) # 模拟耗时操作 print(Login test done) def test_search(): # ... 模拟搜索流程 time.sleep(1.5) print(Search test done) def test_browse(): # ... 模拟浏览流程 time.sleep(1) print(Browse test done) with sync_playwright() as p: browser p.chromium.launch(headlessTrue) # 必须顺序调用 test_login() test_search() test_browse() browser.close() # 总耗时约 4.5 秒异步模式并发执行import asyncio from playwright.async_api import async_playwright async def test_login_async(context): page await context.new_page() # ... 异步登录流程 await asyncio.sleep(2) print(Login test done) await page.close() async def test_search_async(context): page await context.new_page() # ... 异步搜索流程 await asyncio.sleep(1.5) print(Search test done) await page.close() async def test_browse_async(context): page await context.new_page() # ... 异步浏览流程 await asyncio.sleep(1) print(Browse test done) await page.close() async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessTrue) # 为每个测试任务创建一个独立的上下文实现完全隔离 context await browser.new_context() # 创建三个协程任务 task1 asyncio.create_task(test_login_async(context)) task2 asyncio.create_task(test_search_async(context)) task3 asyncio.create_task(test_browse_async(context)) # 并发等待所有任务完成 await asyncio.gather(task1, task2, task3) await context.close() await browser.close() asyncio.run(main()) # 总耗时约 2 秒取决于最长的那个任务性能对比分析异步版本通过asyncio.create_task创建了三个并发任务并使用asyncio.gather等待它们全部完成。由于这些任务大部分时间在await asyncio.sleep模拟I/O等待事件循环可以在它们之间高效切换因此总耗时接近于单个最耗时任务的耗时而不是累加。在实际测试中如果每个用例都涉及大量的网络等待和页面加载这种并发带来的速度提升将非常显著。5.2 场景二同时操作多个标签页测试一个“点击按钮打开新标签页并验证”的功能需要你在两个页面间切换操作。同步模式from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page1 context.new_page() page1.goto(https://example.com) # 点击打开新标签页的按钮 with page1.expect_popup() as popup_info: page1.click(a[target_blank]) # 假设这个链接会打开新窗口 page2 popup_info.value # 获取新页面的引用 page2.wait_for_load_state() # 现在可以操作两个页面但仍然是顺序的 print(fPage1 title: {page1.title()}) print(fPage2 title: {page2.title()}) # 在页面间切换操作 page1.bring_to_front() # 将page1激活到前台 page1.fill(input, text in page1) page2.bring_to_front() # 将page2激活到前台 page2.click(button) context.close() browser.close()异步模式import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) context await browser.new_context() page1 await context.new_page() await page1.goto(https://example.com) async with page1.expect_popup() as popup_info: await page1.click(a[target_blank]) page2 await popup_info.value await page2.wait_for_load_state() # 可以并发地对两个页面执行操作如果操作独立 title1_task asyncio.create_task(page1.title()) title2_task asyncio.create_task(page2.title()) titles await asyncio.gather(title1_task, title2_task) print(fPage1 title: {titles[0]}) print(fPage2 title: {titles[1]}) # 顺序操作页面 await page1.bring_to_front() await page1.fill(input, text in page1) await page2.bring_to_front() await page2.click(button) await context.close() await browser.close() asyncio.run(main())场景总结对于多标签页操作如果页面间的操作没有严格的先后依赖关系异步模式可以让你并发地获取页面状态如并发的title()提升效率。但对于有依赖的交互如在page1操作完才能操作page2两种模式在代码结构上差异不大异步模式只是语法上多了await。关键在于异步模式为你提供了“在必要时进行并发”的能力和灵活性。6. 与测试框架的集成Pytest实战单元测试框架是自动化测试的骨架。pytest是Python生态中最主流的测试框架它与Playwright的集成非常顺畅并且对两种模式都提供了良好支持。6.1 同步模式集成直观简单同步模式下你可以像写普通pytest测试一样写Playwright测试。通常我们会使用pytest-playwright插件提供的pagefixture它为你管理了浏览器、上下文和页面的生命周期。首先安装插件pip install pytest-playwright一个简单的同步测试文件# test_sync_example.py def test_login_sync(page): # page 是由pytest-playwright提供的fixture page.goto(https://my-app.com/login) page.fill(#username, testuser) page.fill(#password, testpass) page.click(button[typesubmit]) # 使用Playwright的expect进行断言 from playwright.sync_api import expect expect(page).to_have_url(https://my-app.com/dashboard) expect(page.locator(.welcome-msg)).to_contain_text(testuser) def test_search_sync(page): page.goto(https://my-app.com/search) page.fill(.search-box, Playwright) page.press(.search-box, Enter) expect(page.locator(.results)).to_contain_text(Playwright)运行测试pytest test_sync_example.py -v同步集成的优点配置简单测试函数就是普通的函数符合大多数测试工程师的习惯。pagefixture让你无需关心浏览器的启动和关闭。6.2 异步模式集成发挥最大效能异步模式与pytest集成需要借助pytest-asyncio插件来支持异步测试函数。安装必要插件pip install pytest pytest-asyncio pytest-playwright一个异步测试文件注意pytest.mark.asyncio装饰器的使用# test_async_example.py import pytest from playwright.async_api import Page, expect pytest.mark.asyncio async def test_login_async(page: Page): # page fixture 也支持异步 await page.goto(https://my-app.com/login) await page.fill(#username, async_user) await page.fill(#password, async_pass) await page.click(button[typesubmit]) await expect(page).to_have_url(https://my-app.com/dashboard) await expect(page.locator(.welcome-msg)).to_contain_text(async_user) pytest.mark.asyncio async def test_search_async(page: Page): await page.goto(https://my-app.com/search) await page.fill(.search-box, Async Playwright) await page.press(.search-box, Enter) await expect(page.locator(.results)).to_contain_text(Async Playwright)运行测试的命令是一样的。pytest-playwright插件足够智能当你使用异步测试函数时它会提供异步版本的pagefixture。异步集成的威力结合pytest-xdist插件进行分布式测试时异步模式能更好地利用单机多核资源。你可以在一个worker进程内利用异步并发执行多个测试用例再结合多个worker进程并行从而在CI/CD管道中实现极致的测试执行速度。6.3 全局配置与Fixture进阶在实际项目中我们通常需要一些全局配置比如指定浏览器类型、设置基础URL、用户认证等。这可以通过自定义pytestfixture来实现。创建conftest.py文件同步示例# conftest.py import pytest from playwright.sync_api import Browser, BrowserContext pytest.fixture(scopesession) def browser_type_launch_args(browser_type_launch_args): # 全局启动参数例如禁用沙箱在某些Docker环境中需要 return {**browser_type_launch_args, args: [--disable-dev-shm-usage]} pytest.fixture(scopesession) def browser(browser_type_launch_args, playwright): # 启动一个浏览器实例session级别所有测试共用 browser playwright.chromium.launch(**browser_type_launch_args) yield browser browser.close() pytest.fixture(scopefunction) def context(browser, request): # 为每个测试函数创建一个独立的上下文实现测试隔离 # 可以在这里设置viewport、locale、权限等 context browser.new_context( viewport{width: 1920, height: 1080}, localeen-US, permissions[geolocation] ) yield context context.close() pytest.fixture(scopefunction) def page(context): # 为每个测试函数创建一个新页面 page context.new_page() # 可以在这里设置全局的请求拦截或监听 # page.on(request, lambda request: print(f {request.method} {request.url})) yield page page.close() pytest.fixture def login_page(page): # 一个业务级别的fixture已登录的页面 page.goto(https://my-app.com/login) page.fill(#username, fixture_user) page.fill(#password, fixture_pass) page.click(button[typesubmit]) page.wait_for_url(**/dashboard) return page在测试中你就可以直接使用这些自定义的fixture了def test_with_custom_fixture(login_page): # login_page 已经是一个登录后的页面状态 expect(login_page.locator(.user-menu)).to_be_visible()实操心得合理设计fixture的scopefunction,class,module,session对测试性能影响很大。浏览器实例browser启动较慢适合session级别共享。上下文context隔离性好适合function级别确保测试不相互干扰。页面page最轻量通常也是function级别。7. 常见问题、调试技巧与性能优化即使理解了模式在实际编码和运行中还是会遇到各种问题。这里我汇总了一些高频问题和实战技巧。7.1 同步与异步混用的陷阱这是初学者最容易踩的坑。绝对不要在同步函数中调用异步方法反之亦然。这会导致运行时错误或难以预料的行为。错误示例# 错误在同步函数中尝试await def sync_function(): page.goto(...) # 同步调用 title await page.title() # 语法错误不能在同步函数中使用await # ...正确做法如果你有一个异步的辅助函数需要在同步测试中使用必须使用asyncio.run()来运行它但这通常意味着你需要重构代码结构。更好的设计是明确边界将同步代码和异步代码放在不同的模块或层中。7.2 元素定位失败与超时设置定位不到元素是自动化测试的日常。除了检查选择器是否正确Playwright提供了强大的调试工具和灵活的等待策略。1. 使用Playwright Inspector进行实时调试这是最有效的调试手段。在运行脚本时加入PWDEBUG1环境变量会启动一个带有时间旅行调试功能的UI界面。# 同步脚本 PWDEBUG1 python your_sync_script.py # 异步脚本 PWDEBUG1 python -m asyncio your_async_script.py在Inspector里你可以逐步执行每一条命令查看此时的页面快照、DOM状态和Playwright为你的操作生成的推荐定位器。2. 调整超时时间Playwright的许多方法都有timeout参数默认是30秒。对于慢速网络或复杂页面你可能需要调整。# 同步 page.click(button.slow, timeout60000) # 等待60秒 # 异步 await page.click(button.slow, timeout60000)你也可以设置全局超时# 同步 page.set_default_timeout(60000) # 异步 await page.set_default_timeout(60000)3. 使用更稳健的定位策略文本定位page.click(textSubmit)或page.click(button:has-text(Submit))。按角色定位page.get_by_role(button, nameSubmit)。这是W3C推荐的标准可访问性最好。测试ID定位page.get_by_test_id(submit-button)。需要开发在元素上添加># 同步示例 def route_handler(route): if .jpg in route.request.url or .css in route.request.url: route.abort() # 中止请求 else: route.continue_() # 继续请求 page.route(**/*, route_handler)4. 异步模式下的错误处理在异步代码中未被捕获的异常可能导致事件循环停止。确保使用try...except妥善处理。async def safe_operation(page): try: await page.click(button.unstable) except Exception as e: print(f点击操作失败: {e}) # 可以在这里进行截图、记录日志等清理操作 await page.screenshot(patherror.png) raise # 根据需要决定是否重新抛出异常7.4 典型错误速查表现象可能原因同步可能原因异步解决方案TimeoutError元素未在默认30秒内出现/可操作同左1. 检查选择器。2. 使用PWDEBUG1调试。3. 增加timeout参数。4. 确认页面是否已加载完成用networkidle。Error: Target closed页面或浏览器在操作前被关闭了同左检查代码逻辑确保操作在正确的页面生命周期内执行。避免在with块或fixtureteardown后操作page。脚本执行完浏览器不关闭未正确调用browser.close()或使用上下文管理器异步操作未await完成事件循环已退出确保所有异步操作都被await并使用async with管理生命周期。异步代码不执行或立即结束不适用协程被创建但未被执行未await或未加入事件循环确保最外层有asyncio.run()并且所有协程都被await或通过asyncio.create_task调度。TypeError: object NoneType cant be used in await expression不适用忘记在异步方法前加await检查代码对所有Playwright的异步API调用添加await。截图或录屏是空白操作发生在headless模式下页面可能未渲染同左1. 调试时使用headlessFalse。2. 确保在正确的时机截图如操作后等待一下。3. 检查Viewport设置。我个人在实际项目中的体会是从同步模式入门快速搭建起核心测试用例的框架这是非常高效的选择。当用例数量达到数百个CI/CD执行时间成为团队反馈的瓶颈时再系统地评估和迁移到异步模式。迁移过程并不恐怖因为API是一致的主要是添加async/await关键字和重构测试运行逻辑。这个过程中你不仅能获得性能的提升更能加深对Python异步编程和现代Web应用运行机制的理解这对测试开发工程师来说是一笔宝贵的财富。最后分享一个小技巧无论是同步还是异步都请务必为你的关键操作和失败场景添加截图和录屏。Playwright在这方面非常简单# 同步 page.screenshot(pathscreenshot.png, full_pageTrue) # 异步 await page.screenshot(pathscreenshot.png, full_pageTrue) # 录屏需要在创建context时启动 context browser.new_context(record_video_dir./videos/) # 测试结束后视频会自动保存这些视觉证据在调试偶发问题和对失败用例进行根因分析时价值连城。