Playwright 测试指南:Next.js 应用程序的 E2E 测试
端到端测试捕获单元测试遗漏的错误 - 身份验证流程中断、表单提交失败、导航错误和跨浏览器渲染问题。 Playwright 由 Microsoft 构建,凭借其多浏览器支持、自动等待功能和强大的调试工具,已成为领先的 E2E 测试框架。
要点
- Playwright 测试通过单个测试套件跨 Chromium、Firefox 和 WebKit 运行
- 自动等待通过在交互之前等待元素可操作来消除不稳定的测试
- 身份验证状态可以在测试之间共享,以加快执行速度
- CI 与 GitHub Actions 集成提供对每个拉取请求的自动化测试
项目设置
安装
npm init playwright@latest
这将安装 Playwright、创建配置文件并下载浏览器二进制文件。对于 Next.js 项目,将 Web 服务器配置为自动启动:
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
timeout: 30000,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: "http://localhost:3000",
screenshot: "only-on-failure",
trace: "on-first-retry",
},
webServer: {
command: "npm run dev",
port: 3000,
reuseExistingServer: !process.env.CI,
},
projects: [
{ name: "chromium", use: { browserName: "chromium" } },
{ name: "firefox", use: { browserName: "firefox" } },
{ name: "webkit", use: { browserName: "webkit" } },
],
});
编写你的第一个测试
基本导航测试
// e2e/navigation.spec.ts
import { test, expect } from "@playwright/test";
test("home page loads and displays title", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/ECOSIRE/);
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});
test("navigation to services page", async ({ page }) => {
await page.goto("/");
await page.getByRole("link", { name: "Services" }).click();
await expect(page).toHaveURL(/services/);
await expect(page.getByRole("heading", { name: /Services/ })).toBeVisible();
});
表单提交测试
test("contact form submission", async ({ page }) => {
await page.goto("/contact");
await page.getByLabel("Name").fill("Test User");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Message").fill("This is a test message");
await page.getByRole("button", { name: "Send" }).click();
await expect(page.getByText("Message sent")).toBeVisible();
});
页面对象模式
将页面交互封装在页面对象中以实现可维护性:
// e2e/pages/login.page.ts
import { Page, expect } from "@playwright/test";
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.page.getByLabel("Email").fill(email);
await this.page.getByLabel("Password").fill(password);
await this.page.getByRole("button", { name: "Sign In" }).click();
}
async expectLoggedIn() {
await expect(this.page).toHaveURL(/dashboard/);
}
async expectError(message: string) {
await expect(this.page.getByText(message)).toBeVisible();
}
}
在测试中使用:
test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("[email protected]", "password123");
await loginPage.expectLoggedIn();
});
认证测试
共享身份验证状态
通过保存身份验证状态避免在每次测试之前登录:
// e2e/auth.setup.ts
import { test as setup } from "@playwright/test";
setup("authenticate", async ({ page }) => {
await page.goto("/login");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Password").fill("admin-password");
await page.getByRole("button", { name: "Sign In" }).click();
await page.waitForURL("/dashboard");
// Save authentication state
await page.context().storageState({ path: ".auth/user.json" });
});
在 playwright.config.ts 中配置:
projects: [
{ name: "setup", testMatch: /auth\.setup\.ts/ },
{
name: "authenticated",
dependencies: ["setup"],
use: { storageState: ".auth/user.json" },
},
],
视觉回归测试
通过屏幕截图比较捕捉意外的视觉变化:
test("home page visual regression", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("home-page.png", {
maxDiffPixelRatio: 0.01,
});
});
第一次运行时,Playwright 会保存基线屏幕截图。后续运行将与基线进行比较,如果差异超过阈值,则失败。当有意更改时,使用 --update-snapshots 更新基线。
API 模拟
用于确定性测试的模拟 API 响应:
test("displays products from API", async ({ page }) => {
await page.route("/api/products", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{ id: 1, name: "Product A", price: 29.99 },
{ id: 2, name: "Product B", price: 49.99 },
]),
});
});
await page.goto("/products");
await expect(page.getByText("Product A")).toBeVisible();
await expect(page.getByText("Product B")).toBeVisible();
});
CI/CD 集成
GitHub 操作
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
失败的测试报告作为调试工件上传,包括屏幕截图、跟踪和视频记录。
调试工具
- 剧作家检查器:使用
--debug标志运行测试以逐步执行 - 跟踪查看器:打开跟踪以查看每个操作、网络请求和 DOM 快照
- Codegen:运行
npx playwright codegen来记录浏览器交互并生成测试代码 - VS Code 扩展:使用点击定位元素直接从 VS Code 运行和调试测试
最佳实践
- 使用基于角色的选择器:优先使用 getByRole、getByLabel、getByText 而不是 CSS 选择器
- 避免硬编码等待:信任自动等待而不是 page.waitForTimeout()
- 隔离测试:每个测试应该是独立的,不依赖于其他测试
- 测试用户流程,而不是实现:关注用户做什么,而不是代码如何工作
- Keep tests fast: Use API shortcuts for setup (create test data via API, not UI)
- 在 CI 中运行:每个 Pull 请求在合并之前都应该通过 E2E 测试
常见问题
问:Playwright 与 Cypress 相比如何?
Playwright 本身支持多种浏览器(Chromium、Firefox、WebKit),而 Cypress 主要针对 Chromium。 Playwright 的并行执行速度更快,并且可以处理多个选项卡/窗口。 Cypress 的学习曲线稍微容易一些,并且时间旅行调试能力更强。
问:我们如何处理不稳定的测试?
剧作家的自动等待消除了大多数不稳定的情况。对于剩余问题:使用 Web 优先断言(期望定位器),避免测试依赖于时间的行为,并在 CI 中配置重试。跟踪报告有助于确定间歇性故障的根本原因。
问:我们应该测试每个页面吗?
专注于关键用户流程:身份验证、核心业务工作流程、表单提交和支付流程。并非每个页面都需要 E2E 测试——使用单元和集成测试来实现组件级覆盖。
问:E2E 测试需要多长时间?
个别测试应在 30 秒内完成。中型应用程序的完整套件(50-100 次测试)应在并行执行的情况下在 10 分钟内运行。
下一步是什么
使用 Playwright 进行 E2E 测试可以确保您的应用程序从用户角度正确运行。从关键流程开始,并随着应用程序的增长扩大覆盖范围。
联系 ECOSIRE 获取测试自动化帮助,或探索我们的 Odoo 实施服务 进行有质量保证的 ERP 部署。
由 ECOSIRE 发布——帮助企业利用企业软件解决方案进行扩展。
作者
ECOSIRE Research and Development Team
在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。