切换语言
切换主题

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',   // 失败时录屏
  },
});

几个容易踩的坑

  1. webServer.timeout 一定要够长。我一开始设了 30 秒,结果 Next.js 首次冷启动要编译,经常超时。现在设 120 秒,稳了。

  2. 本地开发记得设 reuseExistingServer: true。不然每次跑测试都会重启 Next.js,等得人崩溃。

  3. workers 数量别设太多。我之前设成 CPU 核数,结果测试跑着跑着电脑就卡死了。现在设成核数的一半,既快又稳。

  4. 移动端测试可选。如果你的 Next.js 项目是响应式的,加上 Mobile Chrome 测试能抓到一些移动端特有的 bug。不过会让测试时间翻倍,看需求决定。

配置搞定后,跑一下官方示例测试:

npx playwright test

如果看到绿色的 passed,说明环境 OK 了。接下来就能写真实的测试用例了。

页面交互测试最佳实践(Page Object Model)

刚开始写测试时,我把所有代码都塞在一个文件里。测了个登录页面,代码写了 100 多行,各种 page.locatorpage.fillpage.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 测试

我踩过的坑和建议

  1. 别过度封装。不是所有页面都需要 Page Object。如果某个页面只测一次,直接在测试里写就行,别为了 POM 而 POM。

  2. 方法命名要语义化async fillLoginForm()async fillForm() 好懂。半年后回来看代码,你会感谢自己。

  3. 把等待逻辑封装进 Page Object。Playwright 的自动等待很聪明,但有时还是需要手动 waitFor()。把这些逻辑藏在 Page Object 里,测试用例看起来更干净。

  4. 测试数据单独管理。用户名、密码这种测试数据,我会统一放在 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,原来是状态更新的逻辑写错了。

我的实战建议

  1. API 测试要覆盖边界情况。正常流程大家都会测,但是参数缺失、类型错误、权限不足这些情况也得测到。

  2. 测试数据清理。API 测试会往数据库写数据,记得在 afterAll 里清理。我一般用测试专用的数据库,定期清空:

test.afterAll(async ({ request }) => {
  await request.delete('/api/test/cleanup');
});
  1. Mock 外部服务。如果 API 里调了第三方服务(支付、短信),测试时记得 Mock 掉,不然测一次就扣一次钱。

  2. 关注响应时间。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

这个配置能用,但有几个问题:

  1. 每次都装浏览器,慢
  2. 没有测试数据库,API 测试会挂
  3. 报告只能下载下来看,不方便

我的生产级配置

这是我实际用的配置,加了缓存、数据库、报告部署:

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

配置要点解释

  1. 服务容器(services):我用 PostgreSQL 作为测试数据库,这样 API 测试能正常跑。如果你用 MySQL,改成 mysql:8 就行。

  2. 缓存浏览器actions/cache 会缓存 Playwright 的浏览器文件。第一次跑慢,后面就快了。我只装 chromium,因为 CI 环境跑三个浏览器太慢,chromium 够用了。

  3. 数据库迁移npm run db:migrate 在跑测试前先建表。记得在 package.json 里配好这个脚本:

{
  "scripts": {
    "db:migrate": "prisma migrate deploy"
  }
}
  1. 报告部署:主分支的测试报告会自动部署到 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 提供了几个超有用的功能:

  1. 查看 trace:配置文件里我设了 trace: 'on-first-retry',失败时会生成 trace 文件。下载下来用 npx playwright show-trace trace.zip 能重放整个测试过程。

  2. 查看截图和视频:失败时自动截图和录屏,能直观看到页面状态。

  3. 本地重现 CI 环境:用 act 工具能在本地跑 GitHub Actions,调试起来更快:

# 安装 act
brew install act  # macOS
# 或
choco install act  # Windows

# 运行 workflow
act -j test

我踩过的坑

  1. 超时设置要合理。我一开始设 30 分钟,结果有个测试卡住了,浪费了好多 CI 时间。现在设 60 分钟,但会监控哪些测试跑得特别慢。

  2. 并行数量别太多。CI 环境的机器性能一般,worker 开太多反而慢。我设成 2 个 worker,够用了。

  3. 失败重试别设太多次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 画个图表。简单实用。

我的报告使用习惯

  1. 本地开发:直接看终端输出,失败了用 --debug 模式重跑:

    npx playwright test --debug
  2. PR 审查:看 CI 的 HTML 报告,重点关注失败的测试和运行时长。如果某个测试总是超时,可能是代码有问题。

  3. 定期回顾:每周看一次测试覆盖清单,补充缺失的测试用例。

测试报告不只是数字,更重要的是能帮你发现问题、改进流程。

结论

回想半年前那个手动测试到凌晨三点的夜晚,现在的我真的轻松太多了。

Playwright + Next.js 的组合用下来,最大的感受就是省心。配置一次,后面基本不用管。每次提交代码,CI 自动跑测试;每次发版,心里都有底。线上 bug 确实少了很多,产品经理的夺命连环 call 也少了(笑)。

如果你的 Next.js 项目还在手动测试,我的建议是:

  1. 先从核心流程入手。不用一次性覆盖所有功能,先把登录、支付这种关键路径测起来。
  2. 用 Page Object Model。一开始可能觉得麻烦,但长期来看真的省时间。
  3. 接入 CI/CD。自动化测试不自动跑,那还不如不写。
  4. 别追求 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 第三方服务吗?
必须 Mock,原因有三:

• 费用问题:真实调用支付/短信接口会扣费,测一次损失一次
• 速度问题:第三方服务响应慢会拖累测试速度
• 稳定性问题:第三方服务挂了不应该影响你的测试

Playwright 支持网络拦截,可以 Mock API 响应:
await page.route('**/api/payment', route => route.fulfill({ status: 200, body: '{"success": true}' }));

也可以在 Next.js API Routes 里判断环境变量,测试环境直接返回模拟数据。
测试覆盖率要达到多少才算合格?
E2E 测试不看代码覆盖率,看功能覆盖率。

优先级排序:
• 核心功能(登录、支付、下单):必须 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日

评论

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

相关文章