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 TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.