返回首页
⚙️ 后端 / 架构

Playwright + Vitest 实战:现代 Web 测试完整指南 2026

现代 Web 测试 = Vitest(单元/集成)+ Playwright(E2E)+ 视觉回归。本文从 0 到完整测试体系,含 4 个真实项目 + 测试金字塔 + AI 测试。

Playwright · Vitest · 测试 · E2E · 单元测试 · 集成测试 · CI/CD
📰

今日技术简讯

📰 技术简讯 · 2026-08-02

今日聚合 6 条热门技术内容(中文素材优先)。

🤖 AI / LLM

1. Playwright 推出 AI 自动测试

2. Vitest 推出 Browser Mode GA

🎨 前端 / Web

3. Storybook 9 推出 Vitest 集成

  • 链接https://storybook.js.org/blog/9
  • 来源:Storybook
  • 摘要:Storybook 9 推出 Vitest 集成 + Interaction Testing + AI 自动生成组件测试。

⚙️ 后端 / 架构

4. Testcontainers 推出云服务

5. k6 推出 AI 压测

🚀 独立开发 / OPC

6. 即刻"现代 Web 测试"专题


数据来源:掘金 / InfoQ 中文 / 即刻 / 少数派 / HN 采集时间:2026-08-02 09:00 (UTC+8)

📝

今日深度文

Playwright + Vitest 实战:现代 Web 测试完整指南 2026

一句话结论:现代 Web 测试 = Vitest(快)+ Playwright(准)+ AI(自动化)。本文从 0 到完整测试金字塔,含 4 个真实项目 + 覆盖率优化 + CI/CD。

背景

2026 年现代 Web 测试工具链:

测试金字塔:
        /\
       /E2E\          ← Playwright(少而精)
      /------\
     /  集成  \       ← Vitest + Testing Library
    /----------\
   /    单元    \     ← Vitest(多而快)
  /--------------\

为什么现代测试必备:

  1. 质量保证:自动化发现 bug
  2. 重构安全:不怕改坏
  3. 文档化:测试即文档
  4. AI 时代:AI 写代码必须配 AI 测
  5. CI/CD 必备:自动化部署前提

6 大核心优势

1. Vitest 比 Jest 快 10x

// Vitest 配置(vite.config.ts)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    // 浏览器模式(与 8/2 digest 关联)
    browser: {
      enabled: true,
      provider: 'playwright',
      instances: [
        { browser: 'chromium' },
        { browser: 'firefox' },
        { browser: 'webkit' },
      ],
    },
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html', 'lcov'],
      thresholds: {
        lines: 80,
        functions: 80,
        branches: 80,
        statements: 80,
      },
    },
  },
});

2. Playwright E2E 跨浏览器

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
    { name: 'mobile', use: { ...devices['iPhone 15'] } },
  ],
  
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

3. Testing Library 用户视角

// ❌ 错误:测试实现细节
import { render } from '@testing-library/react';

test('counter', () => {
  const { getByTestId } = render(<Counter />);
  expect(getByTestId('count')).toHaveTextContent('0');
});

// ✅ 正确:测试用户行为
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('点击按钮增加计数', async () => {
  const user = userEvent.setup();
  render(<Counter />);
  
  const button = screen.getByRole('button', { name: /increment/i });
  expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
  
  await user.click(button);
  await user.click(button);
  
  expect(screen.getByText(/count: 2/i)).toBeInTheDocument();
});

4. AI 自动测试

// Playwright AI Mode(2026 新特性)
import { test, expect } from '@playwright/test';

test('AI 自动测试登录流程', async ({ page }) => {
  // 1. 录制用户操作(AI 自动)
  await page.ai.act('访问登录页');
  await page.ai.act('输入邮箱 test@example.com');
  await page.ai.act('输入密码 password123');
  await page.ai.act('点击登录按钮');
  
  // 2. AI 自动断言
  await expect(page.ai.assert('看到 Dashboard')).resolves.toBeTruthy();
});

5. 视觉回归测试

// Playwright 视觉回归
import { test, expect } from '@playwright/test';

test('首页视觉回归', async ({ page }) => {
  await page.goto('/');
  
  // 全页截图对比
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    maxDiffPixels: 100,
  });
});

test('深色主题视觉', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' });
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage-dark.png');
});

6. 性能测试

// k6 AI 压测(与 8/2 digest 关联)
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 100 },
    { duration: '1m', target: 500 },
    { duration: '30s', target: 1000 },
  ],
};

export default function () {
  const res = http.get('https://example.com');
  check(res, {
    'status is 200': (r) => r.status === 200,
    'p95 < 500ms': (r) => r.timings.duration < 500,
  });
}

4 个实战项目

项目 1:单元测试(Vitest)

// lib/utils.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate, slugify, calculateDiscount } from './utils';

describe('utils', () => {
  describe('formatDate', () => {
    it('格式化日期', () => {
      expect(formatDate(new Date('2026-08-02'))).toBe('2026-08-02');
    });
    
    it('处理无效输入', () => {
      expect(() => formatDate(null as any)).toThrow();
    });
  });
  
  describe('slugify', () => {
    it('中英文转换', () => {
      expect(slugify('Hello World')).toBe('hello-world');
      expect(slugify('你好 世界')).toBe('ni-hao-shi-jie');
    });
  });
  
  describe('calculateDiscount', () => {
    it('基础折扣', () => {
      expect(calculateDiscount(100, 0.2)).toBe(80);
    });
    
    it('边界条件', () => {
      expect(calculateDiscount(100, 0)).toBe(100);
      expect(calculateDiscount(100, 1)).toBe(0);
      expect(calculateDiscount(100, 2)).toBe(0); // 超过 100% 当 100%
    });
  });
});

项目 2:组件测试(Vitest + Testing Library)

// components/Counter.test.tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter', () => {
  it('初始计数为 0', () => {
    render(<Counter />);
    expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
  });
  
  it('点击按钮增加', async () => {
    const user = userEvent.setup();
    render(<Counter />);
    
    await user.click(screen.getByRole('button', { name: /increment/i }));
    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });
  
  it('调用 onChange 回调', async () => {
    const onChange = vi.fn();
    const user = userEvent.setup();
    render(<Counter onChange={onChange} />);
    
    await user.click(screen.getByRole('button'));
    expect(onChange).toHaveBeenCalledWith(1);
  });
});

项目 3:E2E 测试(Playwright)

// e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('登录流程', () => {
  test('成功登录', async ({ page }) => {
    await page.goto('/login');
    
    // 1. 填写表单
    await page.getByLabel('邮箱').fill('test@example.com');
    await page.getByLabel('密码').fill('password123');
    
    // 2. 提交
    await page.getByRole('button', { name: '登录' }).click();
    
    // 3. 验证跳转
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('欢迎回来')).toBeVisible();
  });
  
  test('错误密码显示提示', async ({ page }) => {
    await page.goto('/login');
    
    await page.getByLabel('邮箱').fill('test@example.com');
    await page.getByLabel('密码').fill('wrong-password');
    await page.getByRole('button', { name: '登录' }).click();
    
    await expect(page.getByText('密码错误')).toBeVisible();
    await expect(page).toHaveURL('/login');  // 不跳转
  });
  
  test('AI 录制的复杂流程', async ({ page }) => {
    // AI 自动识别用户行为
    await page.ai.act('登录系统');
    await page.ai.act('创建一个新项目');
    await page.ai.act('上传一张图片');
    
    await expect(page.ai.assert('看到新创建的项目')).resolves.toBeTruthy();
  });
});

项目 4:API 集成测试

// e2e/api.spec.ts
import { test, expect } from '@playwright/test';

test('API 完整流程', async ({ request }) => {
  // 1. 创建用户
  const createRes = await request.post('/api/users', {
    data: { name: 'Alice', email: 'alice@example.com' },
  });
  expect(createRes.status()).toBe(201);
  const { id } = await createRes.json();
  
  // 2. 获取用户
  const getRes = await request.get(`/api/users/${id}`);
  expect(getRes.ok()).toBeTruthy();
  const user = await getRes.json();
  expect(user.name).toBe('Alice');
  
  // 3. 更新用户
  const updateRes = await request.patch(`/api/users/${id}`, {
    data: { name: 'Alice Smith' },
  });
  expect(updateRes.ok()).toBeTruthy();
  
  // 4. 删除用户
  const deleteRes = await request.delete(`/api/users/${id}`);
  expect(deleteRes.status()).toBe(204);
  
  // 5. 验证已删除
  const get404 = await request.get(`/api/users/${id}`);
  expect(get404.status()).toBe(404);
});

5 个常见坑

坑 1:测试覆盖率 100% 陷阱

❌ 追求 100% 覆盖率
✅ 关键路径 80%+ 即可

测试不是为了覆盖率,是为了信心

坑 2:E2E 测试太多

❌ 100+ E2E 测试(慢 + 不稳定)
✅ 关键路径 10-20 个 E2E(快 + 稳定)

坑 3:测试实现细节

// ❌ 测试 state
expect(wrapper.state().count).toBe(1);

// ✅ 测试行为
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();

坑 4:异步测试 race condition

// ❌ 等待固定时间
await new Promise(r => setTimeout(r, 1000));

// ✅ 等待特定条件
await expect.poll(() => screen.getByText('Loaded')).toBeVisible();

坑 5:测试数据库混乱

// ❌ 用真实数据库
// ✅ Testcontainers(与 8/2 digest 关联)
import { PostgreSQLContainer } from '@testcontainers/postgresql';

const container = await new PostgreSQLContainer().start();
const dbUrl = container.getConnectionUri();

CI/CD 集成(与 7/27 关联)

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci
      - run: npm run test:unit -- --coverage
      
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run build
      - run: npm run test:e2e
      
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/

性能对比

测试类型 工具 数量 时间
单元测试 Vitest 500 5s
集成测试 Vitest + Testing Library 100 10s
E2E 测试 Playwright 30 30s
视觉回归 Playwright 20 15s
总计 650 60s

与之前内容的关系

7/17 Rust + Axum     → 后端
7/19 Docker           → 容器化
7/20 OWASP            → 安全
7/22 React 19 RSC     → 前端框架
7/24 Cursor + Claude  → 编程工具
7/27 GitHub Actions   → CI/CD
7/31 Web 性能优化     → 性能
8/1 LLM 工程化        → AI 测试
8/2 Playwright + Vitest → 测试  ← 今天
→ "开发 → 部署 → 安全 → 性能 → 测试"完整闭环

7 天落地路径

Day 1:Vitest 配置

npm install -D vitest @testing-library/react
npx vitest init

Day 2:写第一个测试

// utils.test.ts
import { describe, it, expect } from 'vitest';
describe('utils', () => {
  it('works', () => {
    expect(1 + 1).toBe(2);
  });
});

Day 3:组件测试

npm install -D @testing-library/user-event

Day 4:Playwright 配置

npm install -D @playwright/test
npx playwright init

Day 5:E2E 测试

// e2e/login.spec.ts

Day 6:覆盖率 + 视觉回归

// 覆盖率门槛 + 截图对比

Day 7:CI/CD 集成

# .github/workflows/test.yml

我的看法

现代 Web 测试是 2026 年质量保证的"基石"

  1. 测试金字塔:单元 > 集成 > E2E
  2. 速度优先:Vitest 10x 快于 Jest
  3. AI 增强:Playwright AI Mode
  4. CI 必备:每次 PR 自动测
  5. 覆盖率 80%:关键路径

对独立开发者的建议:

  • 新项目必装:Vitest + Playwright
  • 关键路径 E2E:10-20 个测试
  • 覆盖率 80%:不追求 100%
  • CI 集成:每次 PR 自动跑
  • AI 录制:复杂流程自动化

参考


本文基于 Vitest 3 + Playwright 2,2026 年 8 月最新实战。

📚 同主题文章

⚙️ 后端 / 架构 分类更多