Playwright自动化测试实战:从零搭建现代Web测试框架
1. 项目概述:为什么是 Playwright?
如果你正在为现代 Web 应用的自动化测试头疼,尤其是面对那些充斥着动态加载、复杂交互的单页应用(SPA),那么 Playwright 的出现,很可能就是你的解药。我接触过 Selenium、Puppeteer 等一众工具,最终在项目里全面转向 Playwright,核心原因就一个:它真正理解了测试工程师的痛点,并提供了“Web 优先”的解决方案。
简单说,Playwright 是一个由微软开源的 Node.js 库,它提供了一套统一的 API,可以跨 Chromium、Firefox 和 WebKit 三大浏览器引擎驱动自动化。这听起来和 Selenium 有点像?但它的设计哲学完全不同。Selenium 是基于 WebDriver 协议的“命令-响应”模式,而 Playwright 更像是浏览器的“原生伙伴”,它直接通过 DevTools Protocol 与浏览器内核对话,这意味着更快的执行速度、更强大的控制能力,以及最重要的——自动等待。你再也不用在代码里写满sleep(5)或者各种轮询等待元素出现了,Playwright 内置的智能等待机制,能极大地减少那些令人抓狂的“不稳定测试”。
这个实战指南,就是把我从零开始搭建 Playwright 测试框架,到处理各种复杂场景(文件下载、iframe、API 拦截、并行执行)的经验,系统地梳理出来。无论你是刚入门自动化测试的新手,还是正在评估新工具的老手,都能在这里找到可直接落地的配置、代码和避坑指南。
2. 核心设计思路与框架选型
2.1 Playwright vs. Selenium vs. Puppeteer:我们为什么选它?
在做技术选型时,我们对比了市面上主流的几个方案。Selenium 生态庞大但历史包袱重,WebDriver 的通信开销和等待问题在复杂应用中尤为明显。Puppeteer 性能强劲,但只绑定 Chromium,且更偏向于爬虫和脚本场景,其测试运行器功能相对较弱。
Playwright 可以看作是 Puppeteer 的“全面升级版”,它继承了高性能和深度控制能力,并针对测试场景做了大量优化。它的核心优势在于:
- 跨浏览器一致性:一套 API 覆盖三大浏览器引擎,确保你的应用在 Chrome、Firefox 和 Safari 上表现一致。这对于需要做跨浏览器兼容性测试的团队是刚需。
- 自动等待与 Web 优先断言:这是革命性的。Playwright 在执行点击、输入等操作前,会自动等待元素满足一系列可操作性条件(如可见、启用、稳定等)。它的断言(如
expect(locator).toBeVisible())也是可重试的,会持续轮询直到条件满足或超时。这从根本上减少了因网络延迟或渲染速度导致的测试失败。 - 强大的测试隔离:每个测试用例都运行在一个全新的“浏览器上下文”中,这相当于一个独立的用户会话,拥有独立的 cookies、localStorage,但创建开销极小。这避免了测试间的相互污染,让测试可以安全地并行运行。
- 丰富的工具链:内置的测试生成器、追踪查看器、VS Code 扩展,构成了一个完整的开发调试闭环。特别是追踪查看器,当测试失败时,它能提供一个包含所有步骤截图、网络请求、控制台日志的时间轴,让你无需复现就能快速定位问题。
基于以上几点,对于需要构建稳定、快速、可维护的现代 Web 自动化测试体系的团队,Playwright 是目前最值得投入的技术选择。
2.2 项目结构与技术栈规划
一个易于维护的测试项目,结构清晰至关重要。我们的项目通常采用如下分层结构:
e2e-tests/ ├── package.json ├── playwright.config.ts # 主配置文件 ├── tests/ │ ├── fixtures/ # 测试夹具,如登录状态复用 │ │ └── auth.setup.ts │ ├── pages/ # 页面对象模型(Page Object) │ │ ├── login.page.ts │ │ └── dashboard.page.ts │ ├── specs/ # 测试用例 │ │ ├── login.spec.ts │ │ └── user-flow.spec.ts │ └── utils/ # 工具函数 │ └── helper.ts ├── test-results/ # 测试报告和追踪文件(.gitignore) └── .github/workflows/ # CI/CD 流水线配置 └── playwright.yml技术栈说明:
- 语言:优先推荐TypeScript。Playwright 对 TypeScript 的支持是顶级的,完善的类型提示能极大提升开发效率和代码质量,避免许多低级错误。
- 测试运行器:直接使用 Playwright Test。它是专门为 Playwright 设计的,深度集成,提供了并行、重试、报告等所有你需要的内置功能,无需再集成 Jest 或 Mocha。
- 断言库:使用 Playwright Test 自带的
expect,它经过了扩展,支持对 Locator 的异步断言。 - CI/CD:GitHub Actions 是天然搭档,官方提供了
@playwright/test的 GitHub Action,开箱即用。
3. 环境搭建与核心配置详解
3.1 安装与初始化:一步到位
安装 Playwright 的最佳实践是使用官方的初始化命令,它会帮你处理好一切。
# 1. 初始化一个新的测试项目或进入现有项目目录 npm init playwright@latest这个交互式命令会问你几个问题:
- 选择测试语言:TypeScript 或 JavaScript。(选 TypeScript)
- 测试目录位置:默认
tests或e2e。 - 是否添加 GitHub Actions 工作流:建议选“是”,它会生成一个基础的 CI 配置。
- 是否安装 Playwright 浏览器:一定要选“是”。它会下载 Chromium、Firefox 和 WebKit 到本地
node_modules目录,确保环境一致。
完成后,你会得到playwright.config.ts配置文件、示例测试文件以及package.json中的相关脚本。
注意:如果遇到
playwright install chromium很慢或失败,通常是网络问题。可以尝试设置镜像源:# 设置 npm 镜像(如淘宝源) npm config set registry https://registry.npmmirror.com # 设置 Playwright 二进制下载镜像 PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright npx playwright install对于 CentOS 7 等老系统,需确保 glibc 版本满足要求(如 Playwright 1.54.0 需要 glibc >= 2.31)。若系统版本过低,可考虑在 Docker 容器中运行测试。
3.2 配置文件(playwright.config.ts)深度解析
这是 Playwright 测试的“大脑”,理解每个配置项至关重要。
import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ // 1. 测试目录和文件匹配模式 testDir: './tests/specs', testMatch: '**/*.spec.ts', // 只匹配 .spec.ts 文件 // 2. 全局超时设置 timeout: 30 * 1000, // 每个测试用例的超时时间(毫秒) expect: { timeout: 10 * 1000, // 每个断言的最大等待时间 }, // 3. 是否并行运行以及如何并行 fullyParallel: true, // 尽可能并行运行所有测试文件 workers: process.env.CI ? 2 : undefined, // CI环境固定2个worker,本地根据CPU核心数自动分配 retries: process.env.CI ? 2 : 0, // CI环境失败自动重试2次,提高稳定性 // 4. 报告系统 reporter: [ ['html', { outputFolder: 'playwright-report', open: 'never' }], // 生成漂亮的HTML报告 ['list'], // 在控制台输出简洁结果 ['junit', { outputFile: 'test-results/junit.xml' }], // 用于CI集成(如Jenkins) ], // 5. 全局项目配置:可以定义多套环境(如桌面端、移动端、不同浏览器) projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, }, // 可以添加移动端模拟 // { // name: 'Mobile Safari', // use: { ...devices['iPhone 12'] }, // }, ], // 6. 全局前置/后置钩子 globalSetup: './tests/fixtures/global-setup.ts', // 所有worker启动前执行一次(如启动服务) globalTeardown: './tests/fixtures/global-teardown.ts', // 所有worker结束后执行一次 use: { // 所有测试的默认配置 headless: true, // CI环境无头模式,本地调试可设为 false viewport: { width: 1920, height: 1080 }, ignoreHTTPSErrors: true, // 忽略HTTPS证书错误(用于测试环境) actionTimeout: 15 * 1000, // 每个操作(click, fill)的超时时间 navigationTimeout: 30 * 1000, // 页面导航的超时时间 screenshot: 'only-on-failure', // 仅在失败时截图 video: 'retain-on-failure', // 仅在失败时保留录像 trace: 'retain-on-failure', // 开启追踪,仅在失败时保留(排查神器) }, // 7. Web Server:在运行测试前自动启动你的开发服务器 webServer: { command: 'npm run start', // 启动本地服务的命令 url: 'http://localhost:3000', // 等待该URL返回200 reuseExistingServer: !process.env.CI, // 本地重用已有服务器,CI环境不重用 timeout: 120 * 1000, // 服务器启动超时时间 }, });关键配置心得:
workers:本地开发可以设为undefined以最大化利用 CPU。但在 CI 环境中(如 GitHub Actions 的 2 核机器),建议设置为2或4,避免因资源竞争导致测试变慢或不稳定。retries:在 CI 中设置1-2次重试非常有用,可以过滤掉因网络瞬时波动或资源加载延迟导致的偶发性失败,让测试套件更稳定。但重试会掩盖真正的逻辑错误,所以本地开发通常设为0。trace: 'retain-on-failure':务必开启。当测试失败时,生成的trace.zip文件可以用playwright show-trace命令打开,里面包含了测试每一步的完整上下文(DOM、网络、日志、截图),是排查问题的核武器。webServer:这个配置能让你实现“一键测试”。运行npx playwright test时,它会先启动你的应用,再运行测试,最后关闭应用,非常适合集成到 CI 流水线中。
4. 核心 API 与最佳实践
4.1 定位器(Locator):告别脆弱的 XPath/CSS Selector
Playwright 最棒的特性之一就是它的定位器哲学。它鼓励你使用面向用户的、语义化的定位方式,而不是脆弱的、基于实现细节的 CSS 或 XPath。
import { test, expect } from '@playwright/test'; test('使用最佳定位策略', async ({ page }) => { await page.goto('https://example.com/login'); // ❌ 避免:脆弱的 CSS/XPath,一旦前端样式或结构微调就会失败 await page.click('body > div > form > div:nth-child(2) > input'); await page.fill('#username-input', 'user'); // ID 可能动态生成 // ✅ 推荐:使用语义化、面向用户的定位器 // getByRole: 通过 ARIA 角色定位,最接近用户感知(屏幕阅读器也是这么“看”页面的) await page.getByRole('textbox', { name: '用户名' }).fill('testuser'); await page.getByRole('button', { name: '登录' }).click(); // getByLabel: 通过关联的标签文本定位 await page.getByLabel('邮箱地址').fill('test@example.com'); // getByPlaceholder: 通过占位符文本定位 await page.getByPlaceholder('请输入密码').fill('password123'); // getByTestId: 与前端约定好的测试 ID,最稳定(需前端配合添加>test('自动等待示例', async ({ page }) => { await page.goto('/dynamic-content'); // Playwright 在执行 click 前,会自动等待该元素: // 1. 被附加到 DOM // 2. 可见(非隐藏、非透明、有尺寸) // 3. 启用(非 disabled) // 4. 稳定(未在动画中) // 5. 可接收事件(未被其他元素遮挡) await page.getByRole('button', { name: '加载更多' }).click(); // 断言也会自动重试,直到条件满足(默认5秒) // 这行代码会持续检查,直到列表项数量达到10个,或者超时 await expect(page.locator('.item-list > li')).toHaveCount(10); // 等待导航(如点击后跳转新页面) await page.getByText('跳转到详情').click(); await page.waitForURL('**/details/*'); // 使用通配符匹配URL // 等待网络请求完成(非常适合验证 API 调用) const responsePromise = page.waitForResponse('**/api/user/profile'); await page.getByRole('button', { name: '刷新资料' }).click(); const response = await responsePromise; expect(response.status()).toBe(200); });常见陷阱:
- 动态内容:这是录制脚本失败最常见的原因。录制时,工具会记录一个绝对的选择器路径。但如果元素是动态生成的(如列表项、模态框),其选择器下次可能就变了。解决方案:永远使用上文提到的语义化定位器(
getByRole,getByTestId),或者使用相对定位、文本匹配。 - 过度等待:虽然 Playwright 有自动等待,但某些自定义的、非标准的交互(如一个复杂的 Canvas 绘图完成后才出现按钮)可能需要显式等待。这时应使用
page.waitForFunction等待一个特定的 JavaScript 条件成立,而不是死板的page.waitForTimeout(5000)。
4.3 高级交互与复杂场景处理
现代 Web 应用远不止点击和输入。Playwright 提供了处理各种复杂场景的能力。
文件上传与下载:
test('处理文件', async ({ page }) => { // 1. 文件上传(无需触发系统文件选择框) // 方法一:对于 input[type="file"] 元素,直接设置文件路径 await page.locator('input[type="file"]').setInputFiles(['/path/to/file1.pdf', '/path/to/file2.jpg']); // 方法二:监听文件选择对话框(如果页面弹出了对话框) page.on('filechooser', async (fileChooser) => { await fileChooser.setFiles(['/path/to/file.pdf']); }); await page.getByText('上传文件').click(); // 这会触发文件选择对话框 // 2. 文件下载 // 启动下载监听 const downloadPromise = page.waitForEvent('download'); await page.getByRole('link', { name: '导出报告' }).click(); const download = await downloadPromise; // 获取下载建议的文件名,并保存到指定路径 const suggestedFilename = download.suggestedFilename(); const filePath = `./test-results/downloads/${suggestedFilename}`; await download.saveAs(filePath); // 验证文件确实已下载(例如,检查文件是否存在或内容) const fs = require('fs'); expect(fs.existsSync(filePath)).toBeTruthy(); });处理 iframe 和弹窗:
test('与 iframe 和弹窗交互', async ({ page }) => { await page.goto('/page-with-iframe'); // 1. 定位到 iframe 内部 const iframe = page.frameLocator('iframe[name="embedded-content"]'); // 在 iframe 上下文中操作 await iframe.getByRole('button', { name: '内部按钮' }).click(); // 2. 处理新窗口/标签页 const [newPage] = await Promise.all([ page.context().waitForEvent('page'), // 监听新页面事件 page.getByRole('link', { name: '在新窗口打开' }).click(), // 触发打开新页面 ]); await newPage.waitForLoadState('domcontentloaded'); // 在新页面对象上操作 await newPage.getByText('新页面内容').click(); await newPage.close(); // 操作完后记得关闭 // 3. 处理 JavaScript 弹窗(alert, confirm, prompt) page.on('dialog', async dialog => { console.log(`弹窗消息: ${dialog.message()}`); await dialog.accept(); // 点击“确定” // await dialog.dismiss(); // 点击“取消” // 对于 prompt: await dialog.accept('输入的文字'); }); await page.getByRole('button', { name: '触发确认框' }).click(); });模拟设备与网络条件:
import { devices } from '@playwright/test'; test('移动端测试与网络模拟', async ({ browser }) => { // 1. 模拟移动设备(如 iPhone 12) const iPhone12 = devices['iPhone 12']; const context = await browser.newContext({ ...iPhone12, // 可以覆盖默认设置,如地理位置、权限 geolocation: { longitude: 116.397128, latitude: 39.916527 }, permissions: ['geolocation'], }); const mobilePage = await context.newPage(); await mobilePage.goto('/'); // 2. 模拟慢速网络(如 3G) const slowContext = await browser.newContext(); const slowPage = await slowContext.newPage(); await slowPage.route('**/*', (route) => { // 可以拦截并修改请求,这里我们只是模拟延迟 // 实际项目中,更常用的是 playwright.config 中的 `slowMo` 选项来全局降速观察 route.continue(); }); // 或者使用内置的 network 模拟(需在配置中设置) // context.setOffline(true); // 模拟离线 await mobilePage.close(); await context.close(); });拦截和修改网络请求: 这是 Playwright 非常强大的功能,可以用于 Mock 数据、性能测试或验证 API 调用。
test('拦截网络请求', async ({ page }) => { // 1. 拦截并修改请求(例如,修改请求头或请求体) await page.route('**/api/user', async (route) => { const request = route.request(); const postData = request.postData(); // 可以修改 postData const modifiedData = JSON.stringify({ ...JSON.parse(postData || '{}'), mocked: true }); await route.continue({ postData: modifiedData }); }); // 2. 拦截并直接返回 Mock 响应(不发送真实请求) await page.route('**/api/products', async (route) => { const mockResponse = { status: 200, contentType: 'application/json', body: JSON.stringify([{ id: 1, name: 'Mock Product' }]), }; await route.fulfill(mockResponse); }); // 3. 拦截并中止请求(例如,阻止图片加载以加速测试) await page.route('**/*.{png,jpg,jpeg,svg}', (route) => route.abort()); await page.goto('/'); // 此时页面发出的 /api/products 请求将收到我们的 Mock 数据 });5. 测试组织与高级模式
5.1 页面对象模型(Page Object Model, POM)
POM 是 UI 自动化测试中最重要的设计模式,它将页面的元素定位和交互逻辑封装成类,使测试用例更清晰、更易维护。
// tests/pages/login.page.ts import { Locator, Page } from '@playwright/test'; export class LoginPage { readonly page: Page; readonly usernameInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.usernameInput = page.getByRole('textbox', { name: '用户名' }); this.passwordInput = page.getByLabel('密码'); this.submitButton = page.getByRole('button', { name: '登录' }); this.errorMessage = page.locator('.alert-error'); } async goto() { await this.page.goto('/login'); } async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.submitButton.click(); } async getErrorMessage(): Promise<string | null> { return await this.errorMessage.textContent(); } } // tests/specs/login.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/login.page'; test('用户登录成功', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('validUser', 'validPass'); // 断言跳转或出现成功元素 await expect(page).toHaveURL('/dashboard'); }); test('用户登录失败显示错误信息', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('invalidUser', 'wrongPass'); await expect(loginPage.errorMessage).toBeVisible(); expect(await loginPage.getErrorMessage()).toContain('用户名或密码错误'); });POM 进阶技巧:
- 组合模式:对于大型应用,可以创建“组件对象”(如
HeaderComponent,SidebarComponent),然后在页面对象中组合它们。 - 继承:如果有多个页面共享公共元素(如导航栏),可以创建一个
BasePage类。
5.2 夹具(Fixtures)与测试隔离
Playwright Test 的夹具系统非常强大,它用于建立测试环境(如登录状态)并确保测试间的隔离。
// tests/fixtures/auth.setup.ts import { test as baseTest } from '@playwright/test'; import { DashboardPage } from '../pages/dashboard.page'; // 1. 定义一个扩展了基础测试的夹具 export const test = baseTest.extend<{ dashboardPage: DashboardPage; adminDashboardPage: DashboardPage; }>({ // 2. 一个需要登录的页面夹具 dashboardPage: async ({ page }, use) => { // 这部分在每个使用该夹具的测试开始前执行 await page.goto('/login'); await page.getByRole('textbox', { name: '用户名' }).fill('standard_user'); await page.getByLabel('密码').fill('secret_sauce'); await page.getByRole('button', { name: '登录' }).click(); // 等待登录成功,确保状态已就绪 await expect(page).toHaveURL(/.*inventory.html/); const dashboardPage = new DashboardPage(page); // 将创建好的页面对象传递给测试用例 await use(dashboardPage); // 这部分在每个使用该夹具的测试结束后执行(可选,用于清理) // 例如登出 // await page.getByRole('button', { name: '登出' }).click(); }, // 3. 另一个夹具,使用不同的用户登录 adminDashboardPage: async ({ page }, use) => { await page.goto('/login'); await page.getByRole('textbox', { name: '用户名' }).fill('admin_user'); await page.getByLabel('密码').fill('admin_pass'); await page.getByRole('button', { name: '登录' }).click(); await expect(page.getByText('管理面板')).toBeVisible(); await use(new DashboardPage(page)); }, }); export { expect } from '@playwright/test'; // 重新导出 expect // tests/specs/authorized-flow.spec.ts import { test, expect } from '../fixtures/auth.setup'; // 导入自定义夹具 // 现在测试可以直接使用已登录的 dashboardPage test('普通用户查看商品列表', async ({ dashboardPage }) => { await dashboardPage.goto(); const items = await dashboardPage.getProductItems(); expect(items.length).toBeGreaterThan(0); }); test('管理员可以删除商品', async ({ adminDashboardPage }) => { await adminDashboardPage.goto(); await adminDashboardPage.deleteProduct('某商品ID'); await expect(adminDashboardPage.getSuccessToast()).toBeVisible(); });夹具的核心优势:
- 复用与封装:将通用的准备逻辑(如登录)封装起来,避免每个测试重复编写。
- 自动清理:夹具的
use函数之后的部分可以执行清理,确保测试间不互相影响。 - 灵活组合:测试可以按需使用不同的夹具组合,构建不同的测试上下文。
5.3 并行执行与分片(Sharding)
对于大型测试套件,并行执行是缩短反馈周期的关键。Playwright Test 默认就支持并行。
# 运行所有测试,使用所有可用的 CPU 核心并行执行 npx playwright test # 指定 worker 数量 npx playwright test --workers=4 # 在 CI 中,通常使用分片:将测试分成 N 份,在 M 台机器上并行跑 # 机器 1: 运行第一份 npx playwright test --shard=1/3 # 机器 2: 运行第二份 npx playwright test --shard=2/3 # 机器 3: 运行第三份 npx playwright test --shard=3/3在playwright.config.ts中配置fullyParallel: true和workers,Playwright 会自动尝试并行运行所有不相互依赖的测试文件。测试的隔离性(独立的浏览器上下文)是安全并行的前提。
6. 调试、报告与 CI/CD 集成
6.1 调试技巧:让问题无处可藏
当测试失败时,别急着改代码,先利用好 Playwright 的调试工具。
使用追踪查看器(Trace Viewer):
# 首先,确保配置中启用了 trace(建议 'on-first-retry' 或 'retain-on-failure') # 测试失败后,会生成一个 trace.zip 文件 npx playwright show-trace test-results/你的测试-失败-chromium/trace.zip这个图形化工具会展示测试的完整时间线,你可以逐步骤查看当时的页面快照、网络请求、控制台输出,是定位“为什么点击没反应”、“为什么元素找不到”这类问题的最强工具。
使用 VS Code 扩展: 安装官方 “Playwright Test for VSCode” 扩展。它允许你直接在编辑器中运行、调试测试,设置断点,并实时查看浏览器。
无头模式与慢动作:
# 在无头模式下运行,但打开 UI 并放慢动作以便观察 npx playwright test --ui --headed --slow-mo=1000--ui打开 Playwright 的图形化测试运行器,--headed显示浏览器窗口,--slow-mo让每个操作延迟指定的毫秒数。生成并查看截图与录像: 配置
screenshot: 'on'和video: 'on'可以在每次测试时都生成媒体文件,但会占用大量磁盘。通常建议设为'only-on-failure'。
6.2 生成丰富的测试报告
Playwright 支持多种报告格式,HTML 报告是最直观的。
# 运行测试并生成 HTML 报告 npx playwright test --reporter=html # 报告默认生成在 playwright-report 目录,用浏览器打开 index.html 即可 npx playwright show-reportHTML 报告会展示通过率、执行时间、失败的截图和追踪链接。你还可以集成allure-playwright来生成更强大的 Allure 报告。
对于 CI 系统(如 Jenkins、GitLab CI),JUnit 格式的报告是标准。
npx playwright test --reporter=junit --output=test-results/junit.xml6.3 集成到 CI/CD 流水线(以 GitHub Actions 为例)
将 Playwright 测试集成到 CI 中,可以实现代码提交即自动测试。以下是 GitHub Actions 的一个经典配置:
# .github/workflows/playwright.yml name: Playwright Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '18' - name: Install dependencies run: npm ci # 使用 ci 命令确保依赖锁一致 - name: Install Playwright Browsers run: npx playwright install --with-deps chromium # CI中通常只安装一个浏览器以加速 - name: Run Playwright tests run: npx playwright test env: # 传递测试环境变量,如基础URL BASE_URL: ${{ secrets.TEST_ENV_URL }} - name: Upload Playwright report if: always() # 即使测试失败也上传报告 uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 7 - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results path: test-results/ retention-days: 7CI 优化技巧:
- 缓存:缓存
node_modules和 Playwright 浏览器二进制文件可以大幅缩短流水线时间。 - 只安装必要浏览器:CI 中通常只需测试 Chromium,用
npx playwright install chromium。 - 使用官方 Action:微软提供了
@playwright/test的 GitHub Action (microsoft/playwright-github-action),它内置了浏览器安装和优化,更简单。 - 分片与并行:在大型项目中,使用
--shard参数将测试分发到多个 job 中并行执行。
7. 常见问题排查与性能优化
7.1 高频问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
locator.click()超时 | 1. 元素不可见/被遮挡。 2. 元素处于禁用状态。 3. 有动画或过渡效果未完成。 4. 页面仍在加载或渲染。 | 1. 使用locator.hover()或检查元素样式。2. 检查元素 disabled属性。3. 增加 actionTimeout或使用page.waitForFunction等待动画结束。4. 在操作前使用 page.waitForLoadState('networkidle')。 |
expect(locator).toBeVisible()失败 | 1. 断言超时前元素始终未出现。 2. 元素在 DOM 中但 display: none或visibility: hidden。3. 视口外,需要滚动。 | 1. 检查定位器是否正确,或前端逻辑是否有误。 2. 使用 toBeHidden()或检查 CSS。3. 先执行 locator.scrollIntoViewIfNeeded()。 |
| 测试在 CI 中通过,本地失败(或反之) | 1. 环境差异(浏览器版本、屏幕分辨率、时区)。 2. 网络延迟或资源加载速度不同。 3. 测试数据状态不一致。 | 1. 使用 Docker 统一测试环境。 2. CI 中适当增加超时时间 ( timeout,expect.timeout)。3. 使用夹具确保每个测试有干净的初始状态。 |
| 录制脚本回放失败 | 1. 动态内容导致选择器变化(最常见)。 2. 页面加载速度差异。 3. 有未处理的弹窗或导航。 | 1.放弃录制,手写定位器。使用getByRole,getByTestId等稳定定位方式。2. 在关键步骤后添加 page.waitForLoadState。3. 使用 page.on('dialog')处理弹窗。 |
| 文件下载失败或找不到文件 | 1. 下载路径未指定或无权访问。 2. 浏览器设置了“另存为”对话框,未自动下载。 3. 下载链接触发了新窗口。 | 1. 使用download.saveAs()指定绝对路径。2. 在浏览器上下文中设置 acceptDownloads: true。3. 监听 download事件,而不是新页面事件。 |
page.goto()超时 | 1. 网络问题或服务器未响应。 2. 页面有无限重定向。 3. 证书错误(测试环境常见)。 | 1. 检查网络和服务器状态。 2. 使用 page.goto(url, { waitUntil: 'domcontentloaded' })减少等待。3. 配置 ignoreHTTPSErrors: true。 |
7.2 性能优化与最佳实践
- 一个测试只测一件事:保持测试用例简短、独立。一个复杂的用户流可以拆分成多个测试,用夹具共享登录状态。
- 善用
page.route进行 Mock:对于依赖第三方 API 或速度慢的后端接口,在测试中拦截并返回模拟数据,可以极大提升测试速度和稳定性。 - 避免不必要的导航:如果多个测试需要同一个页面状态,使用夹具在同一个页面上执行多个操作,而不是每个测试都重新
page.goto。 - 选择性安装浏览器:在 CI 环境中,如果不需要测试所有浏览器,只安装 Chromium (
npx playwright install chromium) 可以节省大量时间和磁盘空间。 - 合理配置超时:根据应用的实际响应速度,在
playwright.config.ts中设置合理的全局timeout、actionTimeout和navigationTimeout。太短会导致不必要的失败,太长会拖慢测试套件。 - 定期清理测试产物:
test-results和playwright-report目录会积累大量截图、录像和追踪文件。确保.gitignore中包含它们,并在 CI 中设置合理的 artifact 保留时间。
从 Selenium 迁移过来,最大的感受是心智负担的减轻。以前要花大量精力处理同步、等待和跨浏览器差异,现在可以更专注于测试逻辑和业务场景本身。Playwright 的现代化设计,让它不仅仅是又一个自动化工具,而是一个完整的、为质量和效率服务的测试工程解决方案。开始可能会觉得它的 API 和理念需要适应,但一旦用上手,就很难再回去了。