Next.js E2E 测试:Playwright 自动化测试实战指南

凌晨三点,盯着 bug 单上那个标红的”紧急”标签,我第三次在生产环境刷新了同一个页面。支付流程又崩了。测试环境明明好好的,为啥到线上就不行?
回想上周的发布:手动点了三十多个页面,填了十几个表单,在三种浏览器里来回切换——到最后还是漏掉了一个只有在移动端滚动到底部才会显示的按钮。产品经理的微信消息弹出来:“用户反馈说优惠券用不了。“胃突然开始疼。
那天晚上我下定决心:必须上 E2E 测试。不能再这样手动测了,人肉测试迟早会把自己和团队拖垮。
说干就干。选型时我看了一圈,Cypress、Selenium、Puppeteer、Playwright… 最后选了 Playwright,主要是因为它支持多浏览器测试,配置也比 Cypress 简单很多。装上 Playwright 后的第一周,它就帮我抓到了五个我手动测试从来没发现的 bug——有些是只在 Firefox 上才会出现的样式问题,有些是异步接口竞态条件导致的。
这篇文章就聊聊我这半年在 Next.js 项目里用 Playwright 的经验。坦白说,刚开始也踩了不少坑,配置文件改了好几版,测试用例重写过两次。不过现在整个流程跑得挺顺,CI/CD 全自动,每次提交代码都会跑一遍测试,线上 bug 少了一大半。
为什么选择 Playwright(vs Cypress)
Cypress 用过的人应该都知道,配置简单,文档友好,社区也活跃。那为啥我最后没选它?
主要卡在三个地方:
多浏览器支持太弱。Cypress 对 Firefox 和 Safari 的支持一直半死不活,测试主要还是跑在 Chromium 上。听起来好像没啥问题,实际上我就踩过一次大坑——支付页面在 Chrome 上完美运行,到了 Safari 就白屏,原因是用了一个 Safari 不支持的 CSS 属性。Playwright 原生支持 Chromium、Firefox、WebKit 三大引擎,跑一套测试能覆盖主流浏览器。
测试速度。Playwright 的并行能力强很多。Cypress 要串行跑测试,50 个用例能跑十几分钟;Playwright 开 8 个 worker 并行跑,同样的用例五分钟搞定。CI/CD 里每分钟都是钱,这个差距还是挺明显的。
API 设计。说实话,刚从 Cypress 切到 Playwright 时,我有点不习惯——Cypress 的链式调用写起来挺爽的。不过用了一段时间 Playwright 的 async/await,发现这种方式反而更符合现代 JavaScript 的写法,和 Next.js 的 Server Components 风格也更一致。
老实讲,Cypress 也不是说不好。如果你的项目只需要测 Chrome,团队对测试不太熟悉,Cypress 确实更容易上手。它的调试工具做得很棒,时间旅行功能(Time Travel)能直接看到测试的每一步,新手友好。
不过对我这种需求来说,Playwright 更合适:
- 需要跨浏览器测试
- 已经有 Next.js/React 经验,
async/await不陌生 - CI 环境要求快速反馈
- 想测试 API 路由和 SSR 页面
选工具这事儿,没有绝对的好坏,看场景。我的建议是:如果项目还小,团队成员测试经验少,先上 Cypress 快速跑起来;如果已经有一定规模,想长期投入自动化测试,Playwright 会是更好的选择。
Next.js + Playwright 配置实战
装 Playwright 超简单,三条命令搞定:
npm init playwright@latest
# 或者用 pnpm
pnpm create playwright安装过程会问你几个问题,选择建议:
- TypeScript? Yes(强烈建议,类型提示能帮你少踩坑)
- 测试目录?tests(默认就行)
- GitHub Actions? Yes(后面 CI/CD 会用到)
装完之后,项目里会多出这些文件:
your-nextjs-project/
├── tests/ # 测试用例目录
│ └── example.spec.ts
├── playwright.config.ts # Playwright 配置
└── .github/
└── workflows/
└── playwright.yml # CI 配置配置文件踩坑记
初始的 playwright.config.ts 是给普通 Web 项目用的,Next.js 项目需要调整一下。这是我用了半年后觉得最稳的配置:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// 测试目录
testDir: './tests',
// 全局超时:单个测试 30 秒
timeout: 30 * 1000,
// 全局期望超时:元素查找 5 秒
expect: {
timeout: 5000,
},
// 失败时重试次数(CI 环境建议开启)
retries: process.env.CI ? 2 : 0,
// 并行 worker 数量(我的机器是 8 核,所以设 4)
workers: process.env.CI ? 2 : 4,
// 测试报告
reporter: [
['html'], // 生成 HTML 报告
['list'], // 终端输出列表
process.env.CI ? ['github'] : ['list'], // CI 环境用 GitHub 格式
],
// 启动 Next.js 开发服务器
webServer: {
command: 'npm run dev',
port: 3000,
timeout: 120 * 1000, // Next.js 首次启动可能要编译,给足时间
reuseExistingServer: !process.env.CI, // 本地开发复用服务器,省时间
},
// 测试项目(多浏览器配置)
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// 移动端测试(可选)
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
// 全局配置
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // 失败时记录 trace,方便调试
screenshot: 'only-on-failure', // 失败时截图
video: 'retain-on-failure', // 失败时录屏
},
});几个容易踩的坑
webServer.timeout一定要够长。我一开始设了 30 秒,结果 Next.js 首次冷启动要编译,经常超时。现在设 120 秒,稳了。本地开发记得设
reuseExistingServer: true。不然每次跑测试都会重启 Next.js,等得人崩溃。workers数量别设太多。我之前设成 CPU 核数,结果测试跑着跑着电脑就卡死了。现在设成核数的一半,既快又稳。移动端测试可选。如果你的 Next.js 项目是响应式的,加上 Mobile Chrome 测试能抓到一些移动端特有的 bug。不过会让测试时间翻倍,看需求决定。
配置搞定后,跑一下官方示例测试:
npx playwright test如果看到绿色的 passed,说明环境 OK 了。接下来就能写真实的测试用例了。
页面交互测试最佳实践(Page Object Model)
刚开始写测试时,我把所有代码都塞在一个文件里。测了个登录页面,代码写了 100 多行,各种 page.locator、page.fill、page.click 满天飞。后来要改个按钮的选择器,十几个测试文件都得改一遍——崩溃。
后来学了 Page Object Model(POM),代码清爽了好几倍。简单说,就是把页面操作封装成一个类,测试用例只调用方法,不直接操作元素。
不用 POM 的测试(反面教材)
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
test('用户登录', async ({ page }) => {
await page.goto('/login');
// 直接操作元素,代码重复
await page.locator('input[name="email"]').fill('[email protected]');
await page.locator('input[name="password"]').fill('password123');
await page.locator('button[type="submit"]').click();
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('登录失败提示', async ({ page }) => {
await page.goto('/login');
// 又来一遍同样的操作...
await page.locator('input[name="email"]').fill('[email protected]');
await page.locator('input[name="password"]').fill('wrongpass');
await page.locator('button[type="submit"]').click();
await expect(page.locator('.error')).toBeVisible();
});看到问题了吗?如果设计师把 input[name="email"] 改成 input[id="email"],所有测试都得改。
用 POM 重构后(推荐写法)
先创建 Page Object:
// tests/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly dashboardTitle: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error');
this.dashboardTitle = page.locator('h1');
}
// 封装登录操作
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
// 封装导航操作
async goto() {
await this.page.goto('/login');
}
// 封装验证逻辑
async expectLoginSuccess() {
await this.dashboardTitle.waitFor();
await expect(this.dashboardTitle).toContainText('Dashboard');
}
async expectLoginError() {
await expect(this.errorMessage).toBeVisible();
}
}测试用例变得超简洁:
// tests/login.spec.ts
import { test } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('用户登录', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password123');
await loginPage.expectLoginSuccess();
});
test('登录失败提示', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'wrongpass');
await loginPage.expectLoginError();
});爽不爽?现在改选择器,只需要改 LoginPage.ts 一个文件。而且测试代码读起来像自然语言,新人一看就懂。
真实项目中的目录结构
tests/
├── pages/ # Page Objects
│ ├── LoginPage.ts
│ ├── DashboardPage.ts
│ └── CheckoutPage.ts
├── fixtures/ # 测试数据和辅助工具
│ └── testData.ts
├── auth.spec.ts # 认证相关测试
├── checkout.spec.ts # 支付流程测试
└── dashboard.spec.ts # Dashboard 测试我踩过的坑和建议
别过度封装。不是所有页面都需要 Page Object。如果某个页面只测一次,直接在测试里写就行,别为了 POM 而 POM。
方法命名要语义化。
async fillLoginForm()比async fillForm()好懂。半年后回来看代码,你会感谢自己。把等待逻辑封装进 Page Object。Playwright 的自动等待很聪明,但有时还是需要手动
waitFor()。把这些逻辑藏在 Page Object 里,测试用例看起来更干净。测试数据单独管理。用户名、密码这种测试数据,我会统一放在
fixtures/testData.ts,方便管理:
// tests/fixtures/testData.ts
export const testUsers = {
validUser: {
email: '[email protected]',
password: 'password123'
},
invalidUser: {
email: '[email protected]',
password: 'wrongpass'
}
};然后在测试里引用:
import { testUsers } from './fixtures/testData';
await loginPage.login(testUsers.validUser.email, testUsers.validUser.password);这套模式用下来,代码维护成本直线下降。我现在写测试,基本就是:定义 Page Object → 写几行测试用例 → 搞定。
API 路由的 E2E 测试
Next.js 的 API Routes 也是整个应用的一部分,当然也得测。以前我都是用 Postman 手动测接口,累得要死。现在直接在 Playwright 里测,连浏览器都不用开。
Playwright 提供了 request 对象,能直接发 HTTP 请求,特别适合测 Next.js 的 API Routes。
基础 API 测试
先来个简单的例子,测试一个获取用户列表的接口:
// tests/api/users.spec.ts
import { test, expect } from '@playwright/test';
test.describe('用户 API 测试', () => {
test('GET /api/users - 获取用户列表', async ({ request }) => {
const response = await request.get('/api/users');
// 验证状态码
expect(response.status()).toBe(200);
// 验证响应格式
const users = await response.json();
expect(Array.isArray(users)).toBeTruthy();
expect(users.length).toBeGreaterThan(0);
// 验证数据结构
expect(users[0]).toHaveProperty('id');
expect(users[0]).toHaveProperty('email');
expect(users[0]).toHaveProperty('name');
});
test('POST /api/users - 创建用户', async ({ request }) => {
const newUser = {
email: '[email protected]',
name: 'Test User',
password: 'password123'
};
const response = await request.post('/api/users', {
data: newUser
});
expect(response.status()).toBe(201);
const createdUser = await response.json();
expect(createdUser.email).toBe(newUser.email);
expect(createdUser).not.toHaveProperty('password'); // 密码不应该返回
});
test('POST /api/users - 邮箱重复应返回错误', async ({ request }) => {
const duplicateUser = {
email: '[email protected]',
name: 'Duplicate User',
password: 'password123'
};
const response = await request.post('/api/users', {
data: duplicateUser
});
expect(response.status()).toBe(400);
const error = await response.json();
expect(error.message).toContain('邮箱已存在');
});
});带认证的 API 测试
真实项目里,很多接口需要登录后才能访问。这就需要先获取 token,然后在请求头里带上:
// tests/api/auth.spec.ts
import { test, expect } from '@playwright/test';
let authToken: string;
test.describe('需要认证的 API', () => {
// 所有测试前先登录获取 token
test.beforeAll(async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: {
email: '[email protected]',
password: 'password123'
}
});
const { token } = await response.json();
authToken = token;
});
test('GET /api/profile - 获取用户资料', async ({ request }) => {
const response = await request.get('/api/profile', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const profile = await response.json();
expect(profile.email).toBe('[email protected]');
});
test('未登录访问应返回 401', async ({ request }) => {
const response = await request.get('/api/profile');
expect(response.status()).toBe(401);
});
});混合测试:页面 + API
最强大的是把页面测试和 API 测试结合起来。比如测试一个添加文章的功能,我会这样写:
// tests/posts.spec.ts
import { test, expect } from '@playwright/test';
test('发布文章完整流程', async ({ page, request }) => {
// 1. 先通过页面登录
await page.goto('/login');
await page.fill('input[name="email"]', '[email protected]');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// 2. 进入文章编辑页
await page.goto('/posts/new');
await page.fill('input[name="title"]', '测试文章标题');
await page.fill('textarea[name="content"]', '这是测试内容');
await page.click('button:has-text("发布")');
// 3. 等待跳转到文章详情页
await page.waitForURL(/\/posts\/\d+/);
// 4. 通过 API 验证文章确实创建成功
const url = page.url();
const postId = url.split('/').pop();
const response = await request.get(`/api/posts/${postId}`);
expect(response.status()).toBe(200);
const post = await response.json();
expect(post.title).toBe('测试文章标题');
expect(post.content).toBe('这是测试内容');
expect(post.status).toBe('published');
});这种写法的好处是:既测了前端交互,又验证了后端数据。有次我就靠这个发现了一个隐蔽的 bug——页面显示”发布成功”,数据库里文章状态却是 draft,原来是状态更新的逻辑写错了。
我的实战建议
API 测试要覆盖边界情况。正常流程大家都会测,但是参数缺失、类型错误、权限不足这些情况也得测到。
测试数据清理。API 测试会往数据库写数据,记得在
afterAll里清理。我一般用测试专用的数据库,定期清空:
test.afterAll(async ({ request }) => {
await request.delete('/api/test/cleanup');
});Mock 外部服务。如果 API 里调了第三方服务(支付、短信),测试时记得 Mock 掉,不然测一次就扣一次钱。
关注响应时间。Playwright 能拿到请求耗时,我会加个断言确保接口够快:
const start = Date.now();
await request.get('/api/users');
const duration = Date.now() - start;
expect(duration).toBeLessThan(1000); // 接口响应应该在 1 秒内API 测试和页面测试配合好了,基本能覆盖 90% 的场景。剩下那 10% 靠单元测试补。
GitHub Actions CI/CD 集成
测试写完了,下一步就是接入 CI/CD。每次提交代码自动跑测试,这事儿真的能救命——多少次提交前信心满满,CI 跑完才发现自己改挂了别的功能。
好消息是,npm init playwright 已经帮你生成了 GitHub Actions 配置文件。不过那个默认配置比较基础,我会根据实际情况调整。
基础 CI 配置
先看看初始生成的 .github/workflows/playwright.yml:
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30这个配置能用,但有几个问题:
- 每次都装浏览器,慢
- 没有测试数据库,API 测试会挂
- 报告只能下载下来看,不方便
我的生产级配置
这是我实际用的配置,加了缓存、数据库、报告部署:
name: E2E Tests
on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
services:
# 测试数据库(PostgreSQL)
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
NODE_ENV: test
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }}
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run database migrations
run: npm run db:migrate
- name: Run Playwright tests
run: npx playwright test
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 30
# 如果是主分支,把报告部署到 GitHub Pages
- name: Deploy report to GitHub Pages
if: always() && github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./playwright-report配置要点解释
服务容器(services):我用 PostgreSQL 作为测试数据库,这样 API 测试能正常跑。如果你用 MySQL,改成
mysql:8就行。缓存浏览器:
actions/cache会缓存 Playwright 的浏览器文件。第一次跑慢,后面就快了。我只装chromium,因为 CI 环境跑三个浏览器太慢,chromium 够用了。数据库迁移:
npm run db:migrate在跑测试前先建表。记得在package.json里配好这个脚本:
{
"scripts": {
"db:migrate": "prisma migrate deploy"
}
}- 报告部署:主分支的测试报告会自动部署到 GitHub Pages,这样团队成员能直接在线看报告,不用下载。
环境变量配置
测试环境可能需要一些 API key 或密钥。我会在 GitHub 仓库的 Settings → Secrets 里配置:
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_TEST_KEY }}失败时的调试
CI 跑挂了怎么办?Playwright 提供了几个超有用的功能:
查看 trace:配置文件里我设了
trace: 'on-first-retry',失败时会生成 trace 文件。下载下来用npx playwright show-trace trace.zip能重放整个测试过程。查看截图和视频:失败时自动截图和录屏,能直观看到页面状态。
本地重现 CI 环境:用
act工具能在本地跑 GitHub Actions,调试起来更快:
# 安装 act
brew install act # macOS
# 或
choco install act # Windows
# 运行 workflow
act -j test我踩过的坑
超时设置要合理。我一开始设 30 分钟,结果有个测试卡住了,浪费了好多 CI 时间。现在设 60 分钟,但会监控哪些测试跑得特别慢。
并行数量别太多。CI 环境的机器性能一般,worker 开太多反而慢。我设成 2 个 worker,够用了。
失败重试别设太多次。
retries: 2是为了应对偶尔的网络抖动,但如果测试真的有问题,重试再多次也没用,反而延长 CI 时间。
接入 CI/CD 后,代码质量直线上升。现在每个 PR 都必须绿勾才能合并,强制大家重视测试。
测试覆盖率和报告生成
跑完测试后,最重要的是看结果。Playwright 的测试报告做得很棒,信息全面又直观。
HTML 报告(最常用)
测试跑完后,执行:
npx playwright show-report会自动打开一个本地网页,展示所有测试结果。这个报告包含:
- 每个测试的通过/失败状态
- 运行时长
- 失败测试的截图和视频
- Trace 文件(能重放整个测试过程)
我最喜欢的功能是 Trace Viewer。点击失败的测试,能看到测试执行的每一步:网络请求、DOM 快照、控制台日志,全都有。就像时光机一样,能精确定位问题在哪。
测试覆盖率
E2E 测试的覆盖率有点特殊——它不是代码覆盖率,而是功能覆盖率。我会列一个表格,记录哪些功能已经有测试了:
## 测试覆盖清单
### 用户认证
- [x] 登录(正常流程)
- [x] 登录失败(错误密码)
- [x] 注册
- [x] 找回密码
- [ ] 第三方登录(Google)
### 商品管理
- [x] 添加商品
- [x] 编辑商品
- [x] 删除商品
- [ ] 批量导入
### 订单流程
- [x] 加购物车
- [x] 结算
- [x] 支付(模拟环境)
- [ ] 退款流程这个清单会放在项目的 tests/README.md 里,每次加新功能都会更新。一看就知道哪些功能还没测试覆盖。
代码覆盖率(可选)
如果你真的想看代码覆盖率,Playwright 也能做到。需要配置 Istanbul 或 v8 coverage:
// playwright.config.ts
export default defineConfig({
use: {
// 启用代码覆盖率
trace: 'on',
// 注入覆盖率收集代码
contextOptions: {
recordVideo: {
dir: 'test-results/videos'
}
}
}
});不过老实讲,我很少在 E2E 测试里看代码覆盖率。单元测试已经覆盖了核心逻辑,E2E 更关注的是功能流程是否跑通。
自定义报告
有时候需要把测试结果发到 Slack 或钉钉,提醒团队。Playwright 支持自定义 reporter:
// my-reporter.ts
import { Reporter } from '@playwright/test/reporter';
class SlackReporter implements Reporter {
onEnd(result) {
const passed = result.suites.filter(s => s.ok).length;
const failed = result.suites.length - passed;
// 发送到 Slack
fetch('https://hooks.slack.com/services/YOUR_WEBHOOK', {
method: 'POST',
body: JSON.stringify({
text: `测试完成:${passed} 通过,${failed} 失败`
})
});
}
}
export default SlackReporter;在配置文件里启用:
// playwright.config.ts
export default defineConfig({
reporter: [
['html'],
['./my-reporter.ts']
]
});查看测试趋势
如果想看测试的历史趋势(通过率变化、运行时间变化),可以用 Playwright Test Runner 这个在线工具。把 CI 生成的 trace 文件上传上去,能可视化分析测试质量。
不过我更喜欢简单粗暴的方法:每次 CI 跑完,把通过率和运行时间写到一个 CSV 文件里,然后用 Google Sheets 画个图表。简单实用。
我的报告使用习惯
本地开发:直接看终端输出,失败了用
--debug模式重跑:npx playwright test --debugPR 审查:看 CI 的 HTML 报告,重点关注失败的测试和运行时长。如果某个测试总是超时,可能是代码有问题。
定期回顾:每周看一次测试覆盖清单,补充缺失的测试用例。
测试报告不只是数字,更重要的是能帮你发现问题、改进流程。
结论
回想半年前那个手动测试到凌晨三点的夜晚,现在的我真的轻松太多了。
Playwright + Next.js 的组合用下来,最大的感受就是省心。配置一次,后面基本不用管。每次提交代码,CI 自动跑测试;每次发版,心里都有底。线上 bug 确实少了很多,产品经理的夺命连环 call 也少了(笑)。
如果你的 Next.js 项目还在手动测试,我的建议是:
- 先从核心流程入手。不用一次性覆盖所有功能,先把登录、支付这种关键路径测起来。
- 用 Page Object Model。一开始可能觉得麻烦,但长期来看真的省时间。
- 接入 CI/CD。自动化测试不自动跑,那还不如不写。
- 别追求 100% 覆盖率。抓大放小,核心功能测好就行。
最后说个小心得:E2E 测试不只是技术工具,更是团队协作的方式。它让大家都重视代码质量,让沟通成本降低(测试就是最好的文档),让发版变得可预测。
现在我每天五点半就能下班了。省出来的时间,终于能去健身房了——之前办的年卡都快过期了。
开始写测试吧,你的未来自己会感谢你的。
常见问题
Playwright 和 Cypress 到底该选哪个?
• Playwright:跨浏览器测试(Chromium/Firefox/WebKit)、并行速度快(8 worker 比 Cypress 快 3 倍)、async/await 语法、适合中大型项目
• Cypress:Chrome 主导、时间旅行调试功能强、社区资源丰富、新手友好
如果项目只测 Chrome 且团队测试经验少,选 Cypress;如果需要跨浏览器、CI 要求快、团队有 React/Next.js 经验,选 Playwright。
Page Object Model 一定要用吗?
如果项目小(10 个以内测试用例)或页面只测一次,直接在测试里写就行。但如果:
• 多个测试用例操作同一个页面
• 团队有多人维护测试代码
• 项目会长期迭代
那 POM 能让你少踩坑。改一次选择器就知道它的价值了——没有 POM 要改 10+ 个文件,有 POM 只改 1 个。
CI 环境测试总是超时怎么办?
• webServer.timeout 太短:改成 120 秒(Next.js 冷启动要编译)
• worker 数量太多:CI 机器性能一般,设成 2-4 个就够
• 测试本身有问题:用 trace 文件排查,看是网络请求慢还是元素等待超时
• 浏览器安装慢:加 actions/cache 缓存浏览器文件
还有个技巧:只在 CI 跑 chromium,本地再跑多浏览器测试,能快很多。
测试数据怎么管理?每次都要手动清理数据库吗?
• 独立测试数据库:专门用于测试,定期清空,不影响开发环境
• 每次测试后清理:在 test.afterAll() 里调用清理接口,但可能遗漏
• 使用 Docker 容器:每次测试启动全新数据库容器,测完即销毁(最干净但慢)
我用的是第一种 + 第二种结合:CI 用 Docker 容器数据库,本地开发用独立测试库 + afterAll 清理。
API 测试需要 Mock 第三方服务吗?
• 费用问题:真实调用支付/短信接口会扣费,测一次损失一次
• 速度问题:第三方服务响应慢会拖累测试速度
• 稳定性问题:第三方服务挂了不应该影响你的测试
Playwright 支持网络拦截,可以 Mock API 响应:
await page.route('**/api/payment', route => route.fulfill({ status: 200, body: '{"success": true}' }));
也可以在 Next.js API Routes 里判断环境变量,测试环境直接返回模拟数据。
测试覆盖率要达到多少才算合格?
优先级排序:
• 核心功能(登录、支付、下单):必须 100% 覆盖
• 高频功能(浏览商品、加购物车):80% 以上
• 低频功能(找回密码、退款):50% 以上
• 边缘功能(换肤、语言切换):可选
别追求 100% 覆盖,抓大放小。我的项目核心流程 100% 覆盖,整体功能覆盖率 60%,已经能拦住 90% 的 bug。
Playwright 测试写完后,还需要写单元测试吗?
• E2E 测试(Playwright):验证功能流程、用户交互、前后端集成,慢但全面
• 单元测试(Jest/Vitest):验证函数逻辑、边界条件、错误处理,快但局部
理想比例:单元测试 70%,E2E 测试 30%。核心工具函数、hooks、组件逻辑用单元测试,完整用户流程用 E2E 测试。
单元测试能快速定位问题,E2E 测试能确保功能真的能用。两手都要抓。
14 分钟阅读 · 发布于: 2026年1月7日 · 修改于: 2026年1月15日
相关文章
Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战

Next.js 文件上传完整指南:S3/七牛云预签名URL直传实战
Next.js 电商实战:购物车与 Stripe 支付完整实现指南

Next.js 电商实战:购物车与 Stripe 支付完整实现指南
Next.js 单元测试实战:Jest + React Testing Library 完整配置指南


评论
使用 GitHub 账号登录后即可评论