返回首页
⚙️ 后端 / 架构
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 自动测试
- 链接:https://playwright.dev/blog/ai-testing
- 来源:Microsoft
- 摘要:Playwright 推出 AI 自动测试,自然语言生成 E2E 测试 + 自愈机制。
2. Vitest 推出 Browser Mode GA
- 链接:https://vitest.dev/blog/browser-mode
- 来源:Vitest
- 摘要:Vitest Browser Mode GA,浏览器中跑单元测试 + JSDOM 替代品。
🎨 前端 / Web
3. Storybook 9 推出 Vitest 集成
- 链接:https://storybook.js.org/blog/9
- 来源:Storybook
- 摘要:Storybook 9 推出 Vitest 集成 + Interaction Testing + AI 自动生成组件测试。
⚙️ 后端 / 架构
4. Testcontainers 推出云服务
- 链接:https://testcontainers.com/blog/cloud
- 来源:Testcontainers
- 摘要:Testcontainers 推出云服务,临时数据库 / Redis / Kafka 容器,测试环境 0 配置。
5. k6 推出 AI 压测
- 链接:https://k6.io/blog/ai-load-testing
- 来源:Grafana
- 摘要:k6 推出 AI 压测,AI 生成测试场景 + 自动识别瓶颈 + 性能建议。
🚀 独立开发 / OPC
6. 即刻"现代 Web 测试"专题
- 链接:https://m.okjike.com/web-testing-2026
- 来源:即刻
- 摘要:即刻 200+ 独立开发者分享 Web 测试经验,单元测试 / E2E / 性能测试 / 视觉回归。
数据来源:掘金 / 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(多而快)
/--------------\
为什么现代测试必备:
- 质量保证:自动化发现 bug
- 重构安全:不怕改坏
- 文档化:测试即文档
- AI 时代:AI 写代码必须配 AI 测
- 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 年质量保证的"基石":
- 测试金字塔:单元 > 集成 > E2E
- 速度优先:Vitest 10x 快于 Jest
- AI 增强:Playwright AI Mode
- CI 必备:每次 PR 自动测
- 覆盖率 80%:关键路径
对独立开发者的建议:
- 新项目必装:Vitest + Playwright
- 关键路径 E2E:10-20 个测试
- 覆盖率 80%:不追求 100%
- CI 集成:每次 PR 自动跑
- AI 录制:复杂流程自动化
参考
- Vitest
- Playwright
- Testing Library
- Storybook
- Testcontainers
- k6
- GitHub Actions(7/27)
- Web 性能(7/31)
- LLMOps(8/1)
本文基于 Vitest 3 + Playwright 2,2026 年 8 月最新实战。