1. 项目概述:为什么选择 Playwright 作为你的第一个自动化测试工具?

如果你正在寻找一个能让你快速上手、功能强大且对现代 Web 应用支持极佳的自动化测试工具,那么 Playwright 几乎是不二之选。我接触过 Selenium、Puppeteer 等一众工具,最终在项目里全面转向 Playwright,原因很简单:它解决了太多历史遗留的痛点。想象一下,你不再需要为不同浏览器下载和匹配不同版本的驱动,不再需要处理那些恼人的跨域 iframe 或等待元素时的随机超时,甚至能轻松录制你的操作并生成代码——这就是 Playwright 带来的体验。

这个项目,就是带你从零开始,用 Python 搭建第一个真正能跑起来的自动化测试脚本。我们不会停留在简单的“打开网页、点击按钮”,而是会深入到实际工作中你会遇到的场景:如何处理动态加载、如何与复杂表单交互、如何进行断言验证,以及如何组织你的代码让它更易于维护。无论你是测试工程师想提升效率,还是开发同学想为自己的项目补充自动化测试,甚至是运维同学希望通过脚本进行日常巡检,这篇内容都能给你一条清晰的路径。你会发现,写自动化测试脚本,和写普通的 Python 脚本一样直观。

2. 环境搭建与核心工具链解析

工欲善其事,必先利其器。在动手写代码之前,我们需要一个干净、可复现的 Python 环境,并理解 Playwright 工具链的构成。这能避免你未来陷入“在我的机器上能跑”的窘境。

2.1 Python 环境与包管理的最佳实践

我强烈建议你使用 venv conda 来创建独立的虚拟环境。这能确保项目依赖的纯净性,避免与系统或其他项目的 Python 包发生冲突。这里以 venv 为例,因为它足够轻量且是 Python 标准库的一部分。

打开你的终端(Windows 用 CMD 或 PowerShell,Mac/Linux 用 Terminal),进入你计划存放项目的目录,执行以下命令:

# 创建项目目录并进入
mkdir playwright-first-script
cd playwright-first-script

# 创建虚拟环境,环境文件夹名为 .venv
python -m venv .venv

# 激活虚拟环境
# Windows (CMD/PowerShell)
.venv\Scripts\activate
# MacOS/Linux
source .venv/bin/activate

激活后,你的命令行提示符前通常会显示 (.venv) ,这表明你已处于虚拟环境中。接下来安装 Playwright 的 Python 包:

pip install playwright

这个命令会安装 playwright 这个核心库。但请注意,这 并不 包含浏览器本身。Playwright 的设计理念是浏览器作为独立实体进行管理,这带来了更好的隔离性和版本控制。

2.2 Playwright 命令行工具(CLI)的妙用

安装完库后,Playwright 提供了一个强大的命令行工具。首先,我们需要用它来安装浏览器二进制文件:

playwright install

这个命令会下载 Playwright 支持的所有浏览器(Chromium, Firefox, WebKit)的最新稳定版本。如果你只想安装 Chromium(最常用),可以运行 playwright install chromium 。这一步可能会花费一些时间,因为它需要下载几百兆的浏览器文件。完成后,这些浏览器会被存放在一个独立于你系统浏览器的缓存目录中,专供 Playwright 使用。

注意 :在某些企业网络环境下,直接下载可能会失败。你可以通过设置环境变量 PLAYWRIGHT_DOWNLOAD_HOST 来使用镜像源,或者手动下载后指定路径。但作为新手,先尝试直接安装是最简单的。

CLI 的另一个“杀手级”功能是代码生成器。当你对某个网页的操作流程不确定时,可以先用它来录制:

playwright codegen https://www.example.com

执行后,它会打开一个浏览器和一个代码录制面板。你在浏览器中的所有操作(点击、输入、导航)都会被实时转换成 Python(或其他语言)代码,显示在面板中。这对于快速探索和生成脚本骨架极其有用,是你学习和编写脚本的得力助手。

2.3 IDE 选择与配置建议

对于 Python 项目,Visual Studio Code (VSCode) 是目前非常主流的选择。你需要安装 Python 扩展和 Pylance 语言服务器。关键一步是确保 VSCode 使用了我们刚创建的虚拟环境。你可以按 Ctrl+Shift+P (Windows/Linux) 或 Cmd+Shift+P (Mac),输入 “Python: Select Interpreter”,然后选择路径为 ./.venv/Scripts/python.exe (Windows) 或 ./.venv/bin/python (Mac/Linux) 的解释器。

这样配置后,VSCode 的智能提示、代码补全和导入包都会基于你的虚拟环境,避免很多错误。当然,PyCharm 也是极好的选择,其专业版对测试框架的支持更深入。

3. 第一个脚本:从“Hello World”到有意义的测试

现在,让我们告别概念,开始写代码。第一个脚本的目标不是炫技,而是打通从代码到浏览器执行的完整流程,并理解最基本的 API。

3.1 脚本骨架与上下文管理

在你的项目根目录下创建一个文件,命名为 test_demo.py 。我们将从最基础的导入和结构开始:

import asyncio
from playwright.async_api import async_playwright

async def main():
    # 启动 Playwright,它负责管理浏览器进程
    async with async_playwright() as p:
        # 启动一个 Chromium 浏览器实例。`headless=False` 表示我们能看到浏览器界面。
        browser = await p.chromium.launch(headless=False)
        # 创建一个新的浏览器上下文(Context),类似于一个独立的隐身会话。
        context = await browser.new_context()
        # 在上下文中打开一个新页面(Page)。
        page = await context.new_page()

        # --- 你的操作代码将写在这里 ---
        # 例如:导航到一个网页
        await page.goto('https://playwright.dev/python')

        # 等待一段时间以便观察,实际脚本中应使用更智能的等待
        await page.wait_for_timeout(3000)

        # 关闭浏览器
        await browser.close()

# 运行主函数
asyncio.run(main())

代码解析与避坑点

  1. 异步 async/await :Playwright Python 的核心 API 是异步的。这意味着你需要使用 async def 定义函数,并在调用 API 前加 await 。对于新手,记住这个模式即可。如果你非常熟悉同步编程,Playwright 也提供了同步 API ( from playwright.sync_api import sync_playwright ),但异步模式性能更好,是现代 Python 的推荐写法。
  2. Playwright -> Browser -> Context -> Page :这是 Playwright 的核心对象层级。
    • Playwright : 总入口,管理浏览器类型。
    • Browser : 代表一个浏览器进程(如 Chrome)。
    • Context : 非常重要 。它代表一个独立的“浏览器会话”,拥有独立的 cookies、缓存、权限设置。你可以创建多个 Context 来实现测试间的完全隔离,避免状态污染。
    • Page : 代表一个标签页,是你进行大部分操作(导航、点击、输入)的对象。
  3. headless=False :在调试阶段,让浏览器显示出来非常有用,你能直观看到脚本在做什么。当脚本稳定后,可以改为 headless=True 在后台无界面运行,节省资源且更适合持续集成(CI)环境。

3.2 核心操作:导航、定位与交互

让我们给这个骨架注入灵魂,实现一些实际功能。假设我们要在 Playwright 官网搜索文档。

# ... 省略前面的导入和 async with 语句 ...
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context()
        page = await context.new_page()

        # 1. 导航到官网
        await page.goto('https://playwright.dev/python')
        print(f"页面标题是:{await page.title()}")

        # 2. 定位搜索按钮并点击
        # 使用 CSS 选择器定位元素。这里通过 `aria-label` 属性定位。
        await page.click('button[aria-label="Search"]')

        # 3. 在出现的搜索框中输入关键词
        # 等待搜索输入框可见并处于可输入状态
        search_input = page.locator('input[type="search"]')
        await search_input.wait_for(state='visible')
        await search_input.fill('locator')
        print("已输入搜索关键词 'locator'")

        # 4. 等待搜索结果出现并获取第一条结果的文本
        # 使用更精确的定位,等待结果列表的第一个链接
        first_result = page.locator('.DocSearch-Hits a').first
        await first_result.wait_for(state='visible')
        result_text = await first_result.text_content()
        print(f"第一条搜索结果是:{result_text}")

        # 5. 点击第一条结果,导航到新页面
        await first_result.click()
        await page.wait_for_load_state('networkidle') # 等待页面基本加载完成

        # 6. 验证新页面标题是否包含预期内容
        new_title = await page.title()
        assert 'Locator' in new_title, f"页面标题 '{new_title}' 中不包含 'Locator'"
        print("断言通过!页面标题包含 'Locator'。")

        await page.wait_for_timeout(2000)
        await browser.close()
# ... 省略 asyncio.run ...

实操心得

  • page.click() vs locator.click() :上面的例子混用了两种方式。 page.click(selector) 是快捷方法,适用于简单场景。但 page.locator(selector) 是更强大、更推荐的方式 Locator 对象代表一个元素定位策略,它支持链式调用(如 .first , .nth(index) )和更稳定的等待。例如, await page.locator('button').first.click() await page.click('button:first-of-type') 更易读和可靠。
  • 等待策略是稳定的关键 :自动化脚本失败,十有八九是因为“等得不够”或“等错了对象”。Playwright 提供了智能的 auto-wait 机制:在执行点击、输入等操作前,它会自动检查元素是否可见、可交互、稳定等。但有时你需要显式等待,如 wait_for_load_state('networkidle') 等待网络空闲,或 locator.wait_for(state='visible') 等待特定元素出现。 尽量避免使用 page.wait_for_timeout(毫秒) ,这是固定等待,效率低下且不可靠,应作为调试时的临时手段。
  • 定位器(Selector) :Playwright 支持 CSS 选择器、XPath、文本选择器( text= )、React/Vue 组件测试选择器等。 优先使用 CSS 选择器 ,它性能好且可读性高。对于有特定文本的元素, page.locator('text=Submit') 非常直观。XPath 虽然强大,但通常更脆弱,应作为最后的选择。

4. 脚本进阶:处理复杂场景与封装技巧

一个简单的线性脚本远远不够。真实的 Web 应用充满动态内容、弹窗和复杂状态。我们需要让脚本更健壮、更智能。

4.1 处理弹窗、新标签页与框架

现代网页的交互不再局限于单个页面。

# 处理对话框(Alert, Confirm, Prompt)
page.on('dialog', lambda dialog: dialog.accept()) # 监听并自动接受所有对话框

# 处理新标签页(Popup)
async with page.expect_popup() as popup_info:
    await page.click('a[target="_blank"]') # 点击会打开新标签页的链接
new_page = await popup_info.value
# 现在可以在 new_page 上操作了
await new_page.goto('https://example.com')
await new_page.close() # 操作完后关闭新标签页

# 处理 iframe
# 先定位到 iframe 元素
frame_element = page.frame_locator('iframe[name="my-frame"]')
# 然后在 iframe 的上下文中定位元素
button_in_frame = frame_element.locator('button')
await button_in_frame.click()

注意事项 :弹窗监听器 ( page.on('dialog', ...) ) 最好在可能触发弹窗的操作 之前 设置。对于 iframe,如果其 src 是跨域的,Playwright 默认可能无法访问其内容,需要在创建浏览器上下文时配置权限。

4.2 模拟用户输入与设备环境

测试需要模拟真实用户。

# 创建上下文时模拟设备,如 iPhone 11
iphone_11 = p.devices['iPhone 11']
context = await browser.new_context(**iphone_11)

# 模拟地理位置和权限
await context.grant_permissions(['geolocation'])
await context.set_geolocation({'latitude': 52.52, 'longitude': 13.39})

# 模拟网络条件(慢速 3G)
slow_3g = p.request.new_context(
    extra_http_headers={'network-conditions': 'slow-3g'}
)
# 使用这个 context 发起的请求都会受限制

# 文件上传(不再是难题!)
file_input = page.locator('input[type="file"]')
await file_input.set_input_files('/path/to/my/file.pdf')
# 甚至可以上传多个文件
await file_input.set_input_files(['file1.pdf', 'file2.jpg'])

实操心得 set_input_files() 是 Playwright 的一大亮点,它直接设置文件路径,完全绕过了传统自动化工具中需要操作系统文件选择对话框的难题,极其稳定可靠。

4.3 断言与测试结构:引入 Pytest

虽然可以用 Python 自带的 assert ,但结合专业的测试框架如 pytest ,能让测试结构更清晰,报告更美观,并支持夹具(fixture)等高级功能。

首先,安装 pytest 和 pytest-playwright 插件(它提供了有用的 fixture):

pip install pytest pytest-playwright

然后,创建一个测试文件 test_playwright_search.py

import re
import pytest
from playwright.async_api import Page, expect

# 使用 pytest.mark.asyncio 标记异步测试函数
@pytest.mark.asyncio
async def test_search_playwright_docs(page: Page):
    """
    测试 Playwright 官网的搜索功能。
    `page` fixture 由 pytest-playwright 提供,无需手动管理浏览器生命周期。
    """
    # 1. 导航
    await page.goto('https://playwright.dev/python')
    
    # 2. 使用 `expect` API 进行断言,这是 Playwright 推荐的断言方式
    await expect(page).to_have_title(re.compile(r'Playwright'))

    # 3. 执行搜索操作
    await page.click('button[aria-label="Search"]')
    await page.locator('input[type="search"]').fill('locator')
    
    # 4. 断言搜索结果出现
    first_result = page.locator('.DocSearch-Hits a').first
    await expect(first_result).to_be_visible()
    result_text = await first_result.text_content()
    # 使用 expect 断言文本内容
    await expect(first_result).to_contain_text('locator', ignore_case=True)
    
    # 5. 点击并导航
    await first_result.click()
    # 断言新页面 URL 包含特定路径
    await expect(page).to_have_url(re.compile(r'.*/docs/locators$'))

# 可以运行 `pytest test_playwright_search.py -v` 来执行测试

为什么用 expect 而不用 assert expect API 内置了智能等待和丰富的匹配器,它会自动重试直到条件满足或超时,这让断言变得更稳定,是编写可靠测试的最佳实践。

5. 项目组织与持续集成(CI)集成雏形

当脚本多起来时,良好的项目结构至关重要。

5.1 基础项目目录结构

一个可维护的测试项目可能如下所示:

playwright-automation-project/
├── .venv/                     # 虚拟环境(.gitignore 忽略)
├── .gitignore
├── requirements.txt           # 项目依赖
├── pytest.ini                # Pytest 配置文件
├── conftest.py               # Pytest 共享 fixture
├── pages/                    # 页面对象模型(Page Object Model)
│   ├── __init__.py
│   ├── base_page.py          # 基础页面类
│   └── search_page.py        # 搜索页面类
├── tests/                    # 测试用例
│   ├── __init__.py
│   ├── test_search.py
│   └── test_auth.py
├── fixtures/                 # 测试数据
│   └── test_data.json
├── reports/                  # 测试报告输出目录
└── utils/                    # 工具函数
    └── helper.py
  • requirements.txt : 使用 pip freeze > requirements.txt 生成,确保他人能复现环境。
  • pytest.ini : 配置 pytest 行为,例如默认命令行参数。
    [pytest]
    addopts = -v --tb=short --html=reports/report.html --self-contained-html
    asyncio_mode = auto
    
  • conftest.py : 定义全局 fixture,比如自动为每个测试提供 page 对象,并配置浏览器参数。
    import pytest
    from playwright.async_api import async_playwright
    
    @pytest.fixture(scope='session')
    async def browser():
        async with async_playwright() as p:
            # 在 CI 环境中通常以无头模式运行
            browser = await p.chromium.launch(headless=True)
            yield browser
            await browser.close()
    
    @pytest.fixture
    async def page(browser):
        context = await browser.new_context(viewport={'width': 1920, 'height': 1080})
        page = await context.new_page()
        yield page
        await context.close()
    

5.2 在 CI/CD 中运行 Playwright 测试

以 GitHub Actions 为例,创建一个 .github/workflows/playwright.yml 文件:

name: Playwright Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.10'
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          playwright install --with-deps chromium # 只安装 Chromium 及其依赖
      - name: Run tests
        run: pytest tests/ -v
      - name: Upload test report
        if: always() # 即使测试失败也上传报告
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: reports/ # 假设 pytest-html 报告生成在这里

避坑技巧 :在 CI 环境中,确保安装了 Playwright 所需的系统依赖。 playwright install --with-deps 命令在 Linux 上会自动处理这些。如果遇到问题,查阅 Playwright 官方文档的 CI 部分,有针对 GitHub Actions、GitLab CI、Jenkins 等的详细指南。

6. 常见问题排查与性能优化实战记录

即使按照最佳实践,你依然会遇到问题。这里记录了几个我踩过的坑和解决方案。

6.1 元素定位失败:动态内容与等待策略

问题 :脚本报错 TimeoutError: Timeout 30000ms exceeded. ,提示找不到元素。

排查思路

  1. 检查选择器 :首先用浏览器的开发者工具(F12)检查你的 CSS 选择器或 XPath 在当前页面是否唯一匹配。页面结构可能已更改。
  2. 验证元素状态 :元素可能被样式隐藏( display: none , visibility: hidden ),或者被其他元素遮挡。Playwright 的 auto-wait 会检查这些。
  3. 处理动态加载 :最常见的原因。元素是 JavaScript 动态加载的,在脚本执行时尚未出现在 DOM 中。
    • 解决方案 A(推荐) :使用 page.locator(selector).wait_for(state='visible') page.wait_for_selector(selector) 显式等待。
    • 解决方案 B :等待某个网络请求完成。如果元素在某个 API 调用后出现,可以使用 page.wait_for_response('**/api/data')
    • 解决方案 C :等待特定文本出现。 page.wait_for_selector('text=Loading finished')
  4. 使用更稳健的定位器 :优先使用 data-testid 这类专为测试添加的属性。如果不行,尝试结合文本和属性: page.locator('button:has-text("Submit")')

6.2 异步操作与竞态条件

问题 :脚本执行顺序错乱,比如在页面加载完成前就尝试点击。

解决方案 :严格遵守异步编程模式。确保在任何一个 await 操作完成前,不进行依赖于其结果的下一步操作。善用 page.wait_for_load_state() ,它有几个状态:

  • load : 等待 load 事件触发。
  • domcontentloaded : 等待 DOMContentLoaded 事件触发。
  • networkidle : 等待网络活动基本停止(至少 500ms 没有网络请求)。 这是最常用的 ,表示页面主体已加载完毕。
await page.goto('https://example.com')
await page.wait_for_load_state('networkidle') # 等待页面真正“安静”下来
# 现在再开始查找和操作元素

6.3 脚本执行速度慢

问题 :测试套件运行时间过长。

优化策略

  1. 并行执行 :Pytest 可以通过 pytest-xdist 插件并行运行测试。Playwright 支持多个浏览器上下文并行运行,互不干扰。
  2. 复用浏览器上下文 :创建和销毁浏览器开销很大。在 conftest.py 中,将 browser fixture 的 scope 设置为 'session' ,让所有测试复用同一个浏览器进程。为每个测试创建独立的 context page 以实现隔离。
  3. 减少不必要的等待 :用智能等待( wait_for_selector , wait_for_load_state )替代固定的 wait_for_timeout
  4. 禁用非必要资源 :如果测试不关心图片、样式或字体,可以在创建上下文时拦截它们,加速页面加载。
    context = await browser.new_context(
        bypass_csp=True,
        ignore_https_errors=True, # 仅测试环境使用
        # 拦截并中止图片、样式表等请求
        # 实际使用时需谨慎,可能影响页面功能
    )
    
  5. 使用快照(Snapshot)进行视觉比对 :对于检查页面布局是否损坏,视觉回归测试比通过 DOM 属性判断更快更准。Playwright 提供了 page.screenshot() expect(page).to_have_screenshot() 功能。

6.4 在无头环境中运行失败

问题 :脚本在 headless=True (CI环境)下失败,但在 headless=False (本地)下成功。

排查

  • 视口(Viewport)大小 :无头模式默认的视口大小可能与有头模式不同,影响响应式布局。始终在创建页面或上下文时显式设置视口: await browser.new_context(viewport={'width': 1920, 'height': 1080})
  • 权限问题 :某些操作(如通知、地理位置)在无头模式下可能需要额外授权。通过 context.grant_permissions() 提前授予。
  • 字体渲染差异 :极少数情况下,字体缺失可能导致布局细微差异。确保 CI 镜像中安装了基本字体包,或使用 Playwright 的 Docker 镜像,它已包含所需环境。

从一行代码到一套能在 CI 上稳定运行的自动化测试,关键在于理解工具背后的设计理念,并遵循“显式等待、良好隔离、清晰结构”的原则。Playwright 的强大之处在于它为你处理了底层浏览器的复杂性,让你能更专注于测试逻辑本身。开始动手吧,用第一个脚本去打开这扇门,你会发现自动化测试并非遥不可及,而是提升你工作流效率的得力伙伴。如果在实践中遇到具体问题,不妨回头看看“常见问题”部分,或者利用 playwright codegen 这个“外挂”来辅助你理解页面交互。

更多推荐