1. 项目概述为什么是 pytest playwright如果你正在寻找一个能覆盖 Web UI、API、甚至移动端并且编写起来高效、维护起来省心的自动化测试方案那么 pytest 和 Playwright 的组合很可能就是你当前技术栈里的“最优解”。这不仅仅是两个流行工具的简单叠加而是一种理念的契合。pytest 以其简洁的语法和强大的插件生态定义了 Python 测试的现代标准而 Playwright 则以其跨浏览器、跨平台的原生支持、自动等待和强大的录制/调试工具重新定义了端到端测试的体验。当它们结合在一起时你得到的是一套从编写、执行到报告都极其流畅的自动化工作流。我最初从 Selenium 迁移过来时最直观的感受就是“心智负担”大大减轻。以前需要花大量时间处理的隐式/显式等待、不稳定的元素定位、跨浏览器兼容的繁琐配置在 Playwright 这里几乎都成了“开箱即用”的特性。而 pytest 则让测试用例的组织、参数化、夹具fixture管理变得井井有条。这个组合特别适合测试开发工程师、全栈开发者以及任何希望将自动化测试深度集成到 CI/CD 流程中的团队。它不仅能帮你快速搭建起可靠的自动化测试套件更能让你将精力更多地集中在测试逻辑和业务验证本身而不是与工具链搏斗。2. 环境搭建与核心配置解析2.1 基础环境安装与依赖管理上手的第一步是建立一个干净、可复现的 Python 环境。我强烈建议使用venv或conda创建独立的虚拟环境这能避免包版本冲突。# 创建并激活虚拟环境 python -m venv playwright-env source playwright-env/bin/activate # Linux/macOS # 或 playwright-env\Scripts\activate # Windows # 安装核心包 pip install pytest playwright # 安装 Playwright 所需的浏览器Chromium, Firefox, WebKit playwright install这里有个关键点playwright install这个命令。它会下载 Playwright 自带的、版本匹配的浏览器二进制文件到用户目录下。这与 Selenium 需要手动下载并配置 WebDriver 路径的方式截然不同省去了大量环境配置的麻烦也保证了测试环境的一致性。如果你只需要特定浏览器可以使用playwright install chromium或playwright install firefox。接下来初始化一个基本的项目结构。虽然 pytest 和 Playwright 对目录结构没有强制要求但一个清晰的结构能让项目长期健康。my-automation-project/ ├── conftest.py # pytest 全局配置和共享 fixture ├── requirements.txt # 项目依赖 ├── tests/ # 测试用例目录 │ ├── __init__.py │ ├── conftest.py # 测试目录级 fixture可选 │ ├── test_login.py # 测试模块 │ └── test_search.py ├── pages/ # Page Object 模型目录推荐 │ ├── __init__.py │ ├── base_page.py │ └── login_page.py └── utils/ # 工具函数目录 └── helpers.py在requirements.txt中最好固定版本以确保团队协作和 CI 环境的一致性。pytest7.4.4 playwright1.40.0 pytest-playwright0.4.3 # 可选的提供了一些有用的 fixture pytest-html4.1.1 # 用于生成 HTML 报告 pytest-xdist3.5.0 # 用于并行测试2.2 Pytest 核心配置与 Playwright 集成pytest 的威力很大程度上来自于它的配置文件pytest.ini和conftest.py。我们先从pytest.ini开始它用于定义项目级的 pytest 运行行为。# pytest.ini [pytest] # 指定测试文件搜索模式 testpaths tests # 自动发现以 test_ 开头或 _test 结尾的文件和函数 python_files test_*.py *_test.py python_classes Test* *Test python_functions test_* # 添加命令行默认选项 addopts -v --tbshort --strict-markers # 注册自定义标记用于分类测试如 pytest.mark.smoke markers smoke: 冒烟测试用例 regression: 回归测试用例 slow: 运行较慢的测试接下来是重头戏conftest.py。这里是定义fixture的核心场所也是集成 Playwright 的关键。Fixture 是 pytest 的基石它提供了可重用的设置和清理代码。# conftest.py import pytest from playwright.sync_api import Page, BrowserContext, Browser, Playwright pytest.fixture(scopesession) def playwright_instance(): 在整个测试会话中只启动一次 Playwright 实例。 from playwright.sync_api import sync_playwright with sync_playwright() as playwright: yield playwright pytest.fixture(scopesession) def browser(playwright_instance: Playwright): 启动一个浏览器实例会话结束时关闭。 # 这里选择 Chromium也可改为 firefox 或 webkit browser playwright_instance.chromium.launch(headlessFalse) # 调试时可设为 False yield browser browser.close() pytest.fixture def context(browser: Browser): 为每个测试用例创建一个新的浏览器上下文。 上下文相当于一个独立的浏览器会话隔离 cookies、localStorage 等。 这是实现测试隔离的最佳实践。 context browser.new_context( viewport{width: 1920, height: 1080}, ignore_https_errorsTrue # 忽略 HTTPS 证书错误用于测试环境 ) yield context context.close() pytest.fixture def page(context: BrowserContext): 为每个测试用例创建一个新的页面。 这是最常用的 fixture测试函数直接接收它来操作页面。 page context.new_page() yield page page.close()为什么这样设计 Fixture作用域scopeplaywright_instance和browser使用session作用域因为启动和关闭浏览器开销较大在整个测试运行期间只做一次可以显著提升速度。context和page使用默认的function作用域确保每个测试用例都在干净、独立的环境中运行互不干扰这是保证测试稳定性的关键。上下文Context隔离这是 Playwright 比 Selenium 更先进的一个设计。每个context就像是一个全新的浏览器配置文件完全隔离了存储、cookies、权限等。这意味着测试 A 登录后的状态绝不会影响到测试 B。你可以在new_context()中传入很多有用的参数比如模拟移动设备、设置地理位置、权限如摄像头、通知等。自动清理使用yield模式yield 之前的代码是设置之后的代码是清理。pytest 会确保无论测试通过还是失败清理代码browser.close(),context.close(),page.close()都会被执行避免资源泄漏。3. 编写你的第一个高效测试用例3.1 元素定位策略与 Playwright 的优势有了 fixture编写测试用例就变得非常直观。我们先看一个简单的例子测试百度搜索。# tests/test_basic.py def test_baidu_search(page: Page): # 1. 导航到目标网址 page.goto(https://www.baidu.com) # 2. 定位元素并操作 # Playwright 支持多种定位器推荐使用 get_by_* 系列可读性最好 search_box page.get_by_role(textbox, name百度一下) # 通过角色和名称定位 # 或者使用 CSS Selector 或 XPath (应作为最后的选择) # search_box page.locator(#kw) search_box.fill(Playwright 自动化测试) # 3. 点击按钮 search_button page.get_by_role(button, name百度一下) search_button.click() # 4. 断言验证 # Playwright 的断言是自动等待的这是巨大优势。 # 它会等待元素出现、可见、满足条件再执行断言无需额外写 wait。 expect(page).to_have_title(Playwright 自动化测试_百度搜索) # 或者断言某个结果链接存在 first_result page.locator(div.result h3 a).first expect(first_result).to_be_visible() expect(first_result).to_contain_text(Playwright)Playwright 定位器最佳实践优先使用get_by_role()、get_by_text()、get_by_label()这些语义化的定位器最稳定即使前端代码的 CSS 类名或 ID 改变只要角色和文本不变测试就不需要修改。其次使用get_by_test_id()这是最强大的定位方式。需要让开发在元素上添加># pages/base_page.py from playwright.sync_api import Page class BasePage: def __init__(self, page: Page): self.page page self.timeout 30000 # 默认超时时间 def navigate(self, url): self.page.goto(url) def get_title(self): return self.page.title() def wait_for_element(self, locator, statevisible, timeoutNone): timeout timeout or self.timeout self.page.locator(locator).wait_for(statestate, timeouttimeout)# pages/search_page.py from .base_page import BasePage from playwright.sync_api import expect class BaiduSearchPage(BasePage): # 将定位器定义为类的属性清晰集中 SEARCH_INPUT #kw SEARCH_BUTTON #su FIRST_RESULT div.result h3 a nth0 def search_for(self, keyword: str): 执行搜索操作 self.page.locator(self.SEARCH_INPUT).fill(keyword) self.page.locator(self.SEARCH_BUTTON).click() # 可以在这里加入等待结果出现的逻辑 self.page.wait_for_selector(self.FIRST_RESULT, statevisible) def get_first_result_text(self): 获取第一个搜索结果的文本 return self.page.locator(self.FIRST_RESULT).text_content() def assert_first_result_contains(self, expected_text: str): 断言第一个结果包含特定文本 first_result_locator self.page.locator(self.FIRST_RESULT) expect(first_result_locator).to_contain_text(expected_text)# tests/test_with_pom.py from pages.search_page import BaiduSearchPage def test_search_with_pom(page: Page): search_page BaiduSearchPage(page) search_page.navigate(https://www.baidu.com) search_page.search_for(pytest Playwright) search_page.assert_first_result_contains(Playwright) # 或者获取文本后做更复杂的断言 result_text search_page.get_first_result_text() assert pytest in result_text.lower()POM 模式的好处高复用性页面操作逻辑被封装多个测试用例可以复用。低维护成本当页面 UI 变化时你只需要在一个地方Page 类修改定位器所有用到该页面的测试用例都会自动生效。高可读性测试用例读起来像业务脚本“搜索某关键词”而不是一堆技术细节“定位ID为kw的输入框并填充文本”业务和测试人员都更容易理解。4. 高级特性与实战技巧4.1 测试参数化与数据驱动pytest 的pytest.mark.parametrize装饰器可以轻松实现参数化测试用多组数据运行同一个测试逻辑。import pytest # 基础参数化 pytest.mark.parametrize(search_keyword, expected_text, [ (Python, Python), (自动化测试, 自动化), (Playwright, Playwright), ]) def test_baidu_search_parametrize(page: Page, search_keyword, expected_text): page.goto(https://www.baidu.com) page.locator(#kw).fill(search_keyword) page.locator(#su).click() # 简单等待一下结果 page.wait_for_selector(div.result, statevisible, timeout5000) assert expected_text in page.content() # 检查整个页面内容 # 从文件如JSON, CSV或函数加载测试数据更适用于复杂场景 def get_search_data(): return [(数据1, 期望1), (数据2, 期望2)] pytest.mark.parametrize(keyword,expected, get_search_data()) def test_with_external_data(page: Page, keyword, expected): # ... 测试逻辑 ... pass对于更复杂的数据驱动测试例如从 Excel、数据库读取可以结合pytest的fixture来实现。4.2 处理弹窗、iframe 和网络请求现代 Web 应用充满了动态交互Playwright 对此有出色的支持。处理弹窗Dialogdef test_handle_alert(page: Page): # 监听 dialog 事件并在触发时接受accept或驳回dismiss page.on(dialog, lambda dialog: dialog.accept()) # 触发一个 alert 的按钮点击 page.locator(#trigger-alert).click() # 后续断言...处理 iframedef test_iframe_handling(page: Page): page.goto(page-with-iframe.html) # 通过选择器定位到 iframe 元素然后获取其 Frame 对象 frame page.frame_locator(iframe[namemy-frame]) # 或者通过 URL 或名称 # frame page.frame(namemy-frame) # 在 iframe 上下文内进行操作 frame.locator(button.submit).click() expect(frame.locator(.success-message)).to_be_visible()拦截和修改网络请求这是 Playwright 非常强大的功能可以用于模拟后端响应、测试错误场景、或跳过不必要的资源加载以加速测试。def test_intercept_api(page: Page): # 拦截所有类型为 “document” 或 “stylesheet” 的请求并中止加速页面加载用于测试 # page.route(**/*.{png,jpg,jpeg,svg}, lambda route: route.abort()) # 拦截特定 API 请求并返回模拟数据 def handle_route(route): if /api/user in route.request.url: # 返回一个模拟的 JSON 响应 route.fulfill( status200, content_typeapplication/json, bodyjson.dumps({name: Mock User, id: 123}) ) else: # 继续正常的请求 route.continue_() page.route(**/api/**, handle_route) page.goto(https://example.com) # 此时页面调用的 /api/user 将收到我们的模拟数据 expect(page.locator(.user-name)).to_have_text(Mock User)4.3 并行测试与执行控制当测试套件规模增长后串行执行会非常耗时。pytest 可以通过pytest-xdist插件轻松实现并行。# 安装 pip install pytest-xdist # 运行命令使用 4 个 worker 并行执行 pytest tests/ -n 4 # 也可以按逻辑核心数自动分配 pytest tests/ -n auto并行测试的注意事项测试隔离这是并行能正常工作的前提。我们之前用function作用域的page和contextfixture 确保了每个测试用例都有独立的浏览器上下文完美支持并行。资源竞争避免测试用例操作共享的外部资源如同一个测试数据库的某条特定记录。每个测试应该使用独立的数据可以通过在 setup 中生成随机数据如随机用户名来实现。平衡负载可以使用--distloadscope选项让同一个模块内的测试在同一个 worker 中运行有时可以减少一些初始化开销。标记Mark与选择性执行pytest 的标记功能可以灵活地控制哪些测试运行。# 在测试用例上打标记 import pytest pytest.mark.smoke def test_login(page: Page): pass pytest.mark.regression pytest.mark.slow def test_complex_report(page: Page): pass # 运行命令 pytest -m smoke # 只运行冒烟测试 pytest -m not slow # 运行所有非慢速测试 pytest -m regression and not smoke # 运行回归测试中非冒烟的部分5. 报告生成、CI/CD 集成与最佳实践5.1 生成丰富的测试报告清晰的测试报告对于分析结果至关重要。pytest 有多种报告插件。生成 JUnit XML 报告CI 工具友好pytest tests/ --junitxmlreport.xml大多数 CI 系统如 Jenkins, GitLab CI, GitHub Actions都能直接解析 JUnit 格式的报告来展示测试通过率、失败用例等。生成美观的 HTML 报告pip install pytest-html pytest tests/ --htmlreport.html --self-contained-html--self-contained-html选项会将所有 CSS 和图片嵌入到一个 HTML 文件中方便分享。使用 Playwright 内置的 Trace Viewer调试神器当测试失败时光看日志可能不够。Playwright 可以记录测试过程的“轨迹”Trace。# 在 conftest.py 的 page fixture 中配置或在测试中手动控制 pytest.fixture def context(browser, request): # 为每个测试启动跟踪 context browser.new_context() # 开始记录 trace context.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue) yield context # 测试结束后停止并保存 trace 文件 # 使用测试用例的名字作为 trace 文件名的一部分 trace_path ftraces/{request.node.name}.zip context.tracing.stop(pathtrace_path)测试失败后你可以使用 Playwright 命令行工具查看这个 trace 文件它会以一个图形化界面重现测试的每一步操作、网络请求、控制台日志是定位偶发性或复杂问题的终极武器。playwright show-trace traces/test_example.zip5.2 集成到 CI/CD 流水线将自动化测试集成到 CI/CD 中是实现“质量内建”的关键。以下是一个 GitHub Actions 工作流的示例# .github/workflows/playwright.yml name: Playwright Tests on: [push, pull_request] jobs: test: timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install dependencies run: | pip install -r requirements.txt playwright install --with-deps chromium # 只安装 Chromium 及其依赖 - name: Run your tests run: | pytest tests/ --junitxmlreport.xml --htmlreport.html --self-contained-html -n auto env: # 如果你的测试需要环境变量在这里设置 BASE_URL: ${{ secrets.TEST_BASE_URL }} - name: Upload test results if: always() # 无论测试成功失败都上传报告 uses: actions/upload-artifactv4 with: name: playwright-report path: | report.html report.xml traces/ # 如果有 trace 也上传 retention-days: 7CI 环境最佳实践使用 Headless 模式在 CI 中务必以无头模式运行浏览器 (headlessTrue)这更快且不需要图形界面。安装依赖使用playwright install --with-deps确保系统安装所有必要的库如字体、编解码器。并行与重试利用pytest-xdist并行执行并使用pytest-rerunfailures插件对失败用例进行有限次数的重试以应对网络或环境的偶发波动。资源清理确保 CI 任务有超时设置并且工作流在结束后能正确清理运行的浏览器进程避免资源堆积。5.3 项目结构与代码组织最佳实践经过多个项目实践我总结出以下能支撑中大型自动化测试项目的结构project/ ├── .github/workflows/ # CI/CD 配置 ├── config/ # 配置文件 │ ├── __init__.py │ ├── test_config.py # 测试环境配置URL 凭证等 │ └── prod_config.py ├── fixtures/ # 复杂或专用的 fixture │ └── data_fixtures.py # 用于生成测试数据 ├── pages/ # Page Object 类 │ ├── __init__.py │ ├── base_page.py │ ├── login_page.py │ └── dashboard_page.py ├── components/ # 可复用的 UI 组件类如 Modal, Header │ └── modal_component.py ├── tests/ │ ├── __init__.py │ ├── conftest.py # 项目根 conftest │ ├── smoke/ # 按测试类型或模块分目录 │ │ ├── __init__.py │ │ └── test_smoke_login.py │ ├── regression/ │ │ ├── __init__.py │ │ └── test_regression_order.py │ └── api/ # API 测试也可以放在一起 │ └── test_user_api.py ├── utils/ │ ├── __init__.py │ ├── helpers.py # 通用工具函数 │ └── data_generator.py # 测试数据生成器 ├── data/ # 外部测试数据文件 │ └── test_users.json ├── reports/ # 测试报告输出目录.gitignore ├── traces/ # Trace 文件输出目录.gitignore ├── pytest.ini ├── requirements.txt └── README.md关键经验配置与代码分离将环境 URL、账号密码等配置信息放在config模块中通过环境变量或配置文件读取切勿硬编码在测试代码里。组件化思维对于 Header、Sidebar、Modal 等跨页面的通用组件单独封装在components目录下然后在各个 Page 类中组合使用避免重复代码。善用conftest.py的层级项目根目录的conftest.py放全局 fixture如page。在tests/api/子目录下可以再放一个conftest.py定义只用于 API 测试的 fixture如api_client。pytest 会自动发现并合并这些 fixture。测试数据管理对于简单的静态数据可以使用 Python 字典或列表。对于复杂或大量的数据考虑使用 JSON、YAML 文件或工厂模式如factory_boy来动态生成。6. 常见问题排查与性能优化6.1 典型问题与解决方案即使有了优秀的工具在实际编写和运行测试时仍会遇到各种问题。下面是一些常见坑点及解决方法。问题1元素定位不到报TimeoutError这是最常见的问题。检查定位器首先用 Playwright 的录制工具 (playwright codegen) 重新生成定位器确保其准确性。在浏览器开发者工具中测试你的 CSS Selector 或 XPath。检查 iframe目标元素是否在 iframe 内如果是需要先切换到 frame 上下文。检查动态加载元素是否是 AJAX 动态加载的虽然 Playwright 有自动等待但某些复杂场景可能需要更明确的等待。使用page.wait_for_selector()或page.wait_for_function()等待特定条件。检查 Shadow DOM如果元素在 Shadow DOM 内需要使用locator.shadow_root属性来穿透。增加超时时间在慢速环境或网络下可以临时增加操作或等待的超时时间page.click(“button”, timeout10000)。问题2测试在 CI 上通过本地却失败或反之环境差异确保 CI 和本地安装的浏览器版本一致playwright install会解决大部分问题。检查屏幕分辨率、时区等环境变量是否一致。资源加载CI 环境可能屏蔽了某些外部资源如 Google 字体、CDN 图表导致页面布局或脚本加载不同。可以考虑在测试开始时拦截并模拟这些资源或使用page.route来 abort 不必要的请求。使用 Trace这是最强大的调试手段。在 CI 上配置测试失败时自动保存 trace 文件并作为产物上传。下载后在本地用playwright show-trace查看能精确重现 CI 环境下的执行过程。问题3测试执行速度慢并行执行首要方案是使用pytest-xdist。复用 Browser如前所述使用session作用域的browserfixture。减少上下文创建如果测试用例不需要完全隔离例如只是一系列只读操作可以考虑让多个测试用例共享一个context但需注意清理状态。拦截无用请求使用page.route拦截并中止对图片、字体、样式表等静态资源的请求可以大幅提升页面加载速度。这在测试逻辑不依赖页面完整渲染时非常有效。选择 Headless 模式无头模式比有头模式更快。问题4处理非预期弹窗如浏览器通知、证书警告在new_context中设置权限context browser.new_context( permissions[notifications], # 允许或拒绝通知 ignore_https_errorsTrue # 忽略 HTTPS 错误 )监听并处理 dialog如前所述使用page.on(“dialog”, …)。6.2 编写稳定测试的黄金法则拥抱自动等待告别time.sleep彻底放弃使用time.sleep。依赖 Playwright 的内置自动等待和page.wait_for_*系列方法。如果必须等待使用page.wait_for_timeout(毫秒数)作为最后手段并注明原因。使用可靠的定位器优先使用>
pytest与Playwright自动化测试:从环境搭建到CI/CD集成的完整指南
发布时间:2026/6/30 13:43:02
1. 项目概述为什么是 pytest playwright如果你正在寻找一个能覆盖 Web UI、API、甚至移动端并且编写起来高效、维护起来省心的自动化测试方案那么 pytest 和 Playwright 的组合很可能就是你当前技术栈里的“最优解”。这不仅仅是两个流行工具的简单叠加而是一种理念的契合。pytest 以其简洁的语法和强大的插件生态定义了 Python 测试的现代标准而 Playwright 则以其跨浏览器、跨平台的原生支持、自动等待和强大的录制/调试工具重新定义了端到端测试的体验。当它们结合在一起时你得到的是一套从编写、执行到报告都极其流畅的自动化工作流。我最初从 Selenium 迁移过来时最直观的感受就是“心智负担”大大减轻。以前需要花大量时间处理的隐式/显式等待、不稳定的元素定位、跨浏览器兼容的繁琐配置在 Playwright 这里几乎都成了“开箱即用”的特性。而 pytest 则让测试用例的组织、参数化、夹具fixture管理变得井井有条。这个组合特别适合测试开发工程师、全栈开发者以及任何希望将自动化测试深度集成到 CI/CD 流程中的团队。它不仅能帮你快速搭建起可靠的自动化测试套件更能让你将精力更多地集中在测试逻辑和业务验证本身而不是与工具链搏斗。2. 环境搭建与核心配置解析2.1 基础环境安装与依赖管理上手的第一步是建立一个干净、可复现的 Python 环境。我强烈建议使用venv或conda创建独立的虚拟环境这能避免包版本冲突。# 创建并激活虚拟环境 python -m venv playwright-env source playwright-env/bin/activate # Linux/macOS # 或 playwright-env\Scripts\activate # Windows # 安装核心包 pip install pytest playwright # 安装 Playwright 所需的浏览器Chromium, Firefox, WebKit playwright install这里有个关键点playwright install这个命令。它会下载 Playwright 自带的、版本匹配的浏览器二进制文件到用户目录下。这与 Selenium 需要手动下载并配置 WebDriver 路径的方式截然不同省去了大量环境配置的麻烦也保证了测试环境的一致性。如果你只需要特定浏览器可以使用playwright install chromium或playwright install firefox。接下来初始化一个基本的项目结构。虽然 pytest 和 Playwright 对目录结构没有强制要求但一个清晰的结构能让项目长期健康。my-automation-project/ ├── conftest.py # pytest 全局配置和共享 fixture ├── requirements.txt # 项目依赖 ├── tests/ # 测试用例目录 │ ├── __init__.py │ ├── conftest.py # 测试目录级 fixture可选 │ ├── test_login.py # 测试模块 │ └── test_search.py ├── pages/ # Page Object 模型目录推荐 │ ├── __init__.py │ ├── base_page.py │ └── login_page.py └── utils/ # 工具函数目录 └── helpers.py在requirements.txt中最好固定版本以确保团队协作和 CI 环境的一致性。pytest7.4.4 playwright1.40.0 pytest-playwright0.4.3 # 可选的提供了一些有用的 fixture pytest-html4.1.1 # 用于生成 HTML 报告 pytest-xdist3.5.0 # 用于并行测试2.2 Pytest 核心配置与 Playwright 集成pytest 的威力很大程度上来自于它的配置文件pytest.ini和conftest.py。我们先从pytest.ini开始它用于定义项目级的 pytest 运行行为。# pytest.ini [pytest] # 指定测试文件搜索模式 testpaths tests # 自动发现以 test_ 开头或 _test 结尾的文件和函数 python_files test_*.py *_test.py python_classes Test* *Test python_functions test_* # 添加命令行默认选项 addopts -v --tbshort --strict-markers # 注册自定义标记用于分类测试如 pytest.mark.smoke markers smoke: 冒烟测试用例 regression: 回归测试用例 slow: 运行较慢的测试接下来是重头戏conftest.py。这里是定义fixture的核心场所也是集成 Playwright 的关键。Fixture 是 pytest 的基石它提供了可重用的设置和清理代码。# conftest.py import pytest from playwright.sync_api import Page, BrowserContext, Browser, Playwright pytest.fixture(scopesession) def playwright_instance(): 在整个测试会话中只启动一次 Playwright 实例。 from playwright.sync_api import sync_playwright with sync_playwright() as playwright: yield playwright pytest.fixture(scopesession) def browser(playwright_instance: Playwright): 启动一个浏览器实例会话结束时关闭。 # 这里选择 Chromium也可改为 firefox 或 webkit browser playwright_instance.chromium.launch(headlessFalse) # 调试时可设为 False yield browser browser.close() pytest.fixture def context(browser: Browser): 为每个测试用例创建一个新的浏览器上下文。 上下文相当于一个独立的浏览器会话隔离 cookies、localStorage 等。 这是实现测试隔离的最佳实践。 context browser.new_context( viewport{width: 1920, height: 1080}, ignore_https_errorsTrue # 忽略 HTTPS 证书错误用于测试环境 ) yield context context.close() pytest.fixture def page(context: BrowserContext): 为每个测试用例创建一个新的页面。 这是最常用的 fixture测试函数直接接收它来操作页面。 page context.new_page() yield page page.close()为什么这样设计 Fixture作用域scopeplaywright_instance和browser使用session作用域因为启动和关闭浏览器开销较大在整个测试运行期间只做一次可以显著提升速度。context和page使用默认的function作用域确保每个测试用例都在干净、独立的环境中运行互不干扰这是保证测试稳定性的关键。上下文Context隔离这是 Playwright 比 Selenium 更先进的一个设计。每个context就像是一个全新的浏览器配置文件完全隔离了存储、cookies、权限等。这意味着测试 A 登录后的状态绝不会影响到测试 B。你可以在new_context()中传入很多有用的参数比如模拟移动设备、设置地理位置、权限如摄像头、通知等。自动清理使用yield模式yield 之前的代码是设置之后的代码是清理。pytest 会确保无论测试通过还是失败清理代码browser.close(),context.close(),page.close()都会被执行避免资源泄漏。3. 编写你的第一个高效测试用例3.1 元素定位策略与 Playwright 的优势有了 fixture编写测试用例就变得非常直观。我们先看一个简单的例子测试百度搜索。# tests/test_basic.py def test_baidu_search(page: Page): # 1. 导航到目标网址 page.goto(https://www.baidu.com) # 2. 定位元素并操作 # Playwright 支持多种定位器推荐使用 get_by_* 系列可读性最好 search_box page.get_by_role(textbox, name百度一下) # 通过角色和名称定位 # 或者使用 CSS Selector 或 XPath (应作为最后的选择) # search_box page.locator(#kw) search_box.fill(Playwright 自动化测试) # 3. 点击按钮 search_button page.get_by_role(button, name百度一下) search_button.click() # 4. 断言验证 # Playwright 的断言是自动等待的这是巨大优势。 # 它会等待元素出现、可见、满足条件再执行断言无需额外写 wait。 expect(page).to_have_title(Playwright 自动化测试_百度搜索) # 或者断言某个结果链接存在 first_result page.locator(div.result h3 a).first expect(first_result).to_be_visible() expect(first_result).to_contain_text(Playwright)Playwright 定位器最佳实践优先使用get_by_role()、get_by_text()、get_by_label()这些语义化的定位器最稳定即使前端代码的 CSS 类名或 ID 改变只要角色和文本不变测试就不需要修改。其次使用get_by_test_id()这是最强大的定位方式。需要让开发在元素上添加># pages/base_page.py from playwright.sync_api import Page class BasePage: def __init__(self, page: Page): self.page page self.timeout 30000 # 默认超时时间 def navigate(self, url): self.page.goto(url) def get_title(self): return self.page.title() def wait_for_element(self, locator, statevisible, timeoutNone): timeout timeout or self.timeout self.page.locator(locator).wait_for(statestate, timeouttimeout)# pages/search_page.py from .base_page import BasePage from playwright.sync_api import expect class BaiduSearchPage(BasePage): # 将定位器定义为类的属性清晰集中 SEARCH_INPUT #kw SEARCH_BUTTON #su FIRST_RESULT div.result h3 a nth0 def search_for(self, keyword: str): 执行搜索操作 self.page.locator(self.SEARCH_INPUT).fill(keyword) self.page.locator(self.SEARCH_BUTTON).click() # 可以在这里加入等待结果出现的逻辑 self.page.wait_for_selector(self.FIRST_RESULT, statevisible) def get_first_result_text(self): 获取第一个搜索结果的文本 return self.page.locator(self.FIRST_RESULT).text_content() def assert_first_result_contains(self, expected_text: str): 断言第一个结果包含特定文本 first_result_locator self.page.locator(self.FIRST_RESULT) expect(first_result_locator).to_contain_text(expected_text)# tests/test_with_pom.py from pages.search_page import BaiduSearchPage def test_search_with_pom(page: Page): search_page BaiduSearchPage(page) search_page.navigate(https://www.baidu.com) search_page.search_for(pytest Playwright) search_page.assert_first_result_contains(Playwright) # 或者获取文本后做更复杂的断言 result_text search_page.get_first_result_text() assert pytest in result_text.lower()POM 模式的好处高复用性页面操作逻辑被封装多个测试用例可以复用。低维护成本当页面 UI 变化时你只需要在一个地方Page 类修改定位器所有用到该页面的测试用例都会自动生效。高可读性测试用例读起来像业务脚本“搜索某关键词”而不是一堆技术细节“定位ID为kw的输入框并填充文本”业务和测试人员都更容易理解。4. 高级特性与实战技巧4.1 测试参数化与数据驱动pytest 的pytest.mark.parametrize装饰器可以轻松实现参数化测试用多组数据运行同一个测试逻辑。import pytest # 基础参数化 pytest.mark.parametrize(search_keyword, expected_text, [ (Python, Python), (自动化测试, 自动化), (Playwright, Playwright), ]) def test_baidu_search_parametrize(page: Page, search_keyword, expected_text): page.goto(https://www.baidu.com) page.locator(#kw).fill(search_keyword) page.locator(#su).click() # 简单等待一下结果 page.wait_for_selector(div.result, statevisible, timeout5000) assert expected_text in page.content() # 检查整个页面内容 # 从文件如JSON, CSV或函数加载测试数据更适用于复杂场景 def get_search_data(): return [(数据1, 期望1), (数据2, 期望2)] pytest.mark.parametrize(keyword,expected, get_search_data()) def test_with_external_data(page: Page, keyword, expected): # ... 测试逻辑 ... pass对于更复杂的数据驱动测试例如从 Excel、数据库读取可以结合pytest的fixture来实现。4.2 处理弹窗、iframe 和网络请求现代 Web 应用充满了动态交互Playwright 对此有出色的支持。处理弹窗Dialogdef test_handle_alert(page: Page): # 监听 dialog 事件并在触发时接受accept或驳回dismiss page.on(dialog, lambda dialog: dialog.accept()) # 触发一个 alert 的按钮点击 page.locator(#trigger-alert).click() # 后续断言...处理 iframedef test_iframe_handling(page: Page): page.goto(page-with-iframe.html) # 通过选择器定位到 iframe 元素然后获取其 Frame 对象 frame page.frame_locator(iframe[namemy-frame]) # 或者通过 URL 或名称 # frame page.frame(namemy-frame) # 在 iframe 上下文内进行操作 frame.locator(button.submit).click() expect(frame.locator(.success-message)).to_be_visible()拦截和修改网络请求这是 Playwright 非常强大的功能可以用于模拟后端响应、测试错误场景、或跳过不必要的资源加载以加速测试。def test_intercept_api(page: Page): # 拦截所有类型为 “document” 或 “stylesheet” 的请求并中止加速页面加载用于测试 # page.route(**/*.{png,jpg,jpeg,svg}, lambda route: route.abort()) # 拦截特定 API 请求并返回模拟数据 def handle_route(route): if /api/user in route.request.url: # 返回一个模拟的 JSON 响应 route.fulfill( status200, content_typeapplication/json, bodyjson.dumps({name: Mock User, id: 123}) ) else: # 继续正常的请求 route.continue_() page.route(**/api/**, handle_route) page.goto(https://example.com) # 此时页面调用的 /api/user 将收到我们的模拟数据 expect(page.locator(.user-name)).to_have_text(Mock User)4.3 并行测试与执行控制当测试套件规模增长后串行执行会非常耗时。pytest 可以通过pytest-xdist插件轻松实现并行。# 安装 pip install pytest-xdist # 运行命令使用 4 个 worker 并行执行 pytest tests/ -n 4 # 也可以按逻辑核心数自动分配 pytest tests/ -n auto并行测试的注意事项测试隔离这是并行能正常工作的前提。我们之前用function作用域的page和contextfixture 确保了每个测试用例都有独立的浏览器上下文完美支持并行。资源竞争避免测试用例操作共享的外部资源如同一个测试数据库的某条特定记录。每个测试应该使用独立的数据可以通过在 setup 中生成随机数据如随机用户名来实现。平衡负载可以使用--distloadscope选项让同一个模块内的测试在同一个 worker 中运行有时可以减少一些初始化开销。标记Mark与选择性执行pytest 的标记功能可以灵活地控制哪些测试运行。# 在测试用例上打标记 import pytest pytest.mark.smoke def test_login(page: Page): pass pytest.mark.regression pytest.mark.slow def test_complex_report(page: Page): pass # 运行命令 pytest -m smoke # 只运行冒烟测试 pytest -m not slow # 运行所有非慢速测试 pytest -m regression and not smoke # 运行回归测试中非冒烟的部分5. 报告生成、CI/CD 集成与最佳实践5.1 生成丰富的测试报告清晰的测试报告对于分析结果至关重要。pytest 有多种报告插件。生成 JUnit XML 报告CI 工具友好pytest tests/ --junitxmlreport.xml大多数 CI 系统如 Jenkins, GitLab CI, GitHub Actions都能直接解析 JUnit 格式的报告来展示测试通过率、失败用例等。生成美观的 HTML 报告pip install pytest-html pytest tests/ --htmlreport.html --self-contained-html--self-contained-html选项会将所有 CSS 和图片嵌入到一个 HTML 文件中方便分享。使用 Playwright 内置的 Trace Viewer调试神器当测试失败时光看日志可能不够。Playwright 可以记录测试过程的“轨迹”Trace。# 在 conftest.py 的 page fixture 中配置或在测试中手动控制 pytest.fixture def context(browser, request): # 为每个测试启动跟踪 context browser.new_context() # 开始记录 trace context.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue) yield context # 测试结束后停止并保存 trace 文件 # 使用测试用例的名字作为 trace 文件名的一部分 trace_path ftraces/{request.node.name}.zip context.tracing.stop(pathtrace_path)测试失败后你可以使用 Playwright 命令行工具查看这个 trace 文件它会以一个图形化界面重现测试的每一步操作、网络请求、控制台日志是定位偶发性或复杂问题的终极武器。playwright show-trace traces/test_example.zip5.2 集成到 CI/CD 流水线将自动化测试集成到 CI/CD 中是实现“质量内建”的关键。以下是一个 GitHub Actions 工作流的示例# .github/workflows/playwright.yml name: Playwright Tests on: [push, pull_request] jobs: test: timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install dependencies run: | pip install -r requirements.txt playwright install --with-deps chromium # 只安装 Chromium 及其依赖 - name: Run your tests run: | pytest tests/ --junitxmlreport.xml --htmlreport.html --self-contained-html -n auto env: # 如果你的测试需要环境变量在这里设置 BASE_URL: ${{ secrets.TEST_BASE_URL }} - name: Upload test results if: always() # 无论测试成功失败都上传报告 uses: actions/upload-artifactv4 with: name: playwright-report path: | report.html report.xml traces/ # 如果有 trace 也上传 retention-days: 7CI 环境最佳实践使用 Headless 模式在 CI 中务必以无头模式运行浏览器 (headlessTrue)这更快且不需要图形界面。安装依赖使用playwright install --with-deps确保系统安装所有必要的库如字体、编解码器。并行与重试利用pytest-xdist并行执行并使用pytest-rerunfailures插件对失败用例进行有限次数的重试以应对网络或环境的偶发波动。资源清理确保 CI 任务有超时设置并且工作流在结束后能正确清理运行的浏览器进程避免资源堆积。5.3 项目结构与代码组织最佳实践经过多个项目实践我总结出以下能支撑中大型自动化测试项目的结构project/ ├── .github/workflows/ # CI/CD 配置 ├── config/ # 配置文件 │ ├── __init__.py │ ├── test_config.py # 测试环境配置URL 凭证等 │ └── prod_config.py ├── fixtures/ # 复杂或专用的 fixture │ └── data_fixtures.py # 用于生成测试数据 ├── pages/ # Page Object 类 │ ├── __init__.py │ ├── base_page.py │ ├── login_page.py │ └── dashboard_page.py ├── components/ # 可复用的 UI 组件类如 Modal, Header │ └── modal_component.py ├── tests/ │ ├── __init__.py │ ├── conftest.py # 项目根 conftest │ ├── smoke/ # 按测试类型或模块分目录 │ │ ├── __init__.py │ │ └── test_smoke_login.py │ ├── regression/ │ │ ├── __init__.py │ │ └── test_regression_order.py │ └── api/ # API 测试也可以放在一起 │ └── test_user_api.py ├── utils/ │ ├── __init__.py │ ├── helpers.py # 通用工具函数 │ └── data_generator.py # 测试数据生成器 ├── data/ # 外部测试数据文件 │ └── test_users.json ├── reports/ # 测试报告输出目录.gitignore ├── traces/ # Trace 文件输出目录.gitignore ├── pytest.ini ├── requirements.txt └── README.md关键经验配置与代码分离将环境 URL、账号密码等配置信息放在config模块中通过环境变量或配置文件读取切勿硬编码在测试代码里。组件化思维对于 Header、Sidebar、Modal 等跨页面的通用组件单独封装在components目录下然后在各个 Page 类中组合使用避免重复代码。善用conftest.py的层级项目根目录的conftest.py放全局 fixture如page。在tests/api/子目录下可以再放一个conftest.py定义只用于 API 测试的 fixture如api_client。pytest 会自动发现并合并这些 fixture。测试数据管理对于简单的静态数据可以使用 Python 字典或列表。对于复杂或大量的数据考虑使用 JSON、YAML 文件或工厂模式如factory_boy来动态生成。6. 常见问题排查与性能优化6.1 典型问题与解决方案即使有了优秀的工具在实际编写和运行测试时仍会遇到各种问题。下面是一些常见坑点及解决方法。问题1元素定位不到报TimeoutError这是最常见的问题。检查定位器首先用 Playwright 的录制工具 (playwright codegen) 重新生成定位器确保其准确性。在浏览器开发者工具中测试你的 CSS Selector 或 XPath。检查 iframe目标元素是否在 iframe 内如果是需要先切换到 frame 上下文。检查动态加载元素是否是 AJAX 动态加载的虽然 Playwright 有自动等待但某些复杂场景可能需要更明确的等待。使用page.wait_for_selector()或page.wait_for_function()等待特定条件。检查 Shadow DOM如果元素在 Shadow DOM 内需要使用locator.shadow_root属性来穿透。增加超时时间在慢速环境或网络下可以临时增加操作或等待的超时时间page.click(“button”, timeout10000)。问题2测试在 CI 上通过本地却失败或反之环境差异确保 CI 和本地安装的浏览器版本一致playwright install会解决大部分问题。检查屏幕分辨率、时区等环境变量是否一致。资源加载CI 环境可能屏蔽了某些外部资源如 Google 字体、CDN 图表导致页面布局或脚本加载不同。可以考虑在测试开始时拦截并模拟这些资源或使用page.route来 abort 不必要的请求。使用 Trace这是最强大的调试手段。在 CI 上配置测试失败时自动保存 trace 文件并作为产物上传。下载后在本地用playwright show-trace查看能精确重现 CI 环境下的执行过程。问题3测试执行速度慢并行执行首要方案是使用pytest-xdist。复用 Browser如前所述使用session作用域的browserfixture。减少上下文创建如果测试用例不需要完全隔离例如只是一系列只读操作可以考虑让多个测试用例共享一个context但需注意清理状态。拦截无用请求使用page.route拦截并中止对图片、字体、样式表等静态资源的请求可以大幅提升页面加载速度。这在测试逻辑不依赖页面完整渲染时非常有效。选择 Headless 模式无头模式比有头模式更快。问题4处理非预期弹窗如浏览器通知、证书警告在new_context中设置权限context browser.new_context( permissions[notifications], # 允许或拒绝通知 ignore_https_errorsTrue # 忽略 HTTPS 错误 )监听并处理 dialog如前所述使用page.on(“dialog”, …)。6.2 编写稳定测试的黄金法则拥抱自动等待告别time.sleep彻底放弃使用time.sleep。依赖 Playwright 的内置自动等待和page.wait_for_*系列方法。如果必须等待使用page.wait_for_timeout(毫秒数)作为最后手段并注明原因。使用可靠的定位器优先使用>