开发指南测试指南
测试指南
🚀 Cheatsheet (快速执行)
目标: 10分钟内写出第一个自动化测试,确保核心功能不出错
最快测试方式:
# 1. 运行现有测试
bun --filter web e2e
# 2. 写第一个测试
# 创建 tests/my-first.spec.ts
# 3. 测试用户注册流程
立即生效的测试:
import { test, expect } from '@playwright/test'
test('用户可以注册账号', async ({ page }) => {
await page.goto('/auth/signup')
await page.fill('[data-testid="email"]', 'test@example.com')
await page.fill('[data-testid="password"]', 'password123')
await page.click('[data-testid="signup-button"]')
await expect(page).toHaveURL('/app')
})
❓ 为什么要做端到端测试
对 MVP 的价值:
- 避免线上崩溃: 每次部署前自动测试,确保功能正常
- 节省人工测试: 不用每次都手动点击测试注册、登录流程
- 回归测试: 新功能不会破坏旧功能
- 用户体验保障: 从用户角度测试,发现真实问题
不做测试的后果: 改了一行代码,结果注册功能坏了,用户无法使用,直接影响收入。
🤔 为什么选择 Playwright
我们的实战经验:
- 真实浏览器: 在真实的 Chrome、Firefox 中测试
- 快速稳定: 比 Selenium 快很多,很少出现偶发失败
- 强大调试: 可视化界面,看到测试执行过程
- 移动端支持: 可以测试手机版网页
对比其他方案:
# Playwright - ⭐ 推荐,现代化,功能全面
# Cypress - 界面友好,但只支持 Chrome
# Selenium - 老牌工具,但较慢且不稳定
# Puppeteer - 只支持 Chrome,功能有限
🧠 简要原理 & 最简案例
E2E 测试原理: 启动真实浏览器 → 像用户一样操作页面 → 检查结果是否符合预期
核心文件位置:
apps/web/tests/
- 测试文件目录playwright.config.ts
- Playwright 配置.github/workflows/
- CI 自动测试
最简测试示例:
// tests/homepage.spec.ts
import { test, expect } from '@playwright/test'
test('首页加载正常', async ({ page }) => {
// 1. 访问首页
await page.goto('/')
// 2. 检查标题
await expect(page).toHaveTitle(/HackathonWeekly/)
// 3. 检查关键元素
await expect(page.getByRole('button', { name: '立即开始' })).toBeVisible()
})
🛠️ 实际操作步骤
1. 运行现有测试 (2分钟)
# 启动测试 UI
bun --filter web e2e
# 这会:
# 1. 自动启动开发服务器
# 2. 打开 Playwright UI
# 3. 显示所有可用测试
2. 写第一个测试 (5分钟)
创建测试文件:
// apps/web/tests/auth.spec.ts
import { test, expect } from '@playwright/test'
test.describe('用户认证', () => {
test('用户可以注册新账号', async ({ page }) => {
// 访问注册页面
await page.goto('/auth/signup')
// 填写表单
await page.fill('input[name="email"]', 'newuser@test.com')
await page.fill('input[name="password"]', 'password123')
await page.fill('input[name="confirmPassword"]', 'password123')
// 提交表单
await page.click('button[type="submit"]')
// 验证注册成功
await expect(page).toHaveURL('/app')
await expect(page.getByText('欢迎')).toBeVisible()
})
test('用户可以登录', async ({ page }) => {
await page.goto('/auth/login')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/app')
})
})
3. 写业务功能测试 (3分钟)
// tests/core-features.spec.ts
import { test, expect } from '@playwright/test'
test.describe('核心功能', () => {
// 每个测试前先登录
test.beforeEach(async ({ page }) => {
await page.goto('/auth/login')
await page.fill('input[name="email"]', 'test@example.com')
await page.fill('input[name="password"]', 'password123')
await page.click('button[type="submit"]')
await expect(page).toHaveURL('/app')
})
test('用户可以使用 AI 聊天', async ({ page }) => {
await page.goto('/app/chat')
// 发送消息
await page.fill('[data-testid="chat-input"]', '你好')
await page.click('[data-testid="send-button"]')
// 等待 AI 回复
await expect(page.getByText('你好')).toBeVisible()
await expect(page.locator('[data-testid="ai-message"]')).toBeVisible()
})
test('用户可以查看定价页面', async ({ page }) => {
await page.goto('/pricing')
await expect(page.getByText('选择套餐')).toBeVisible()
await expect(page.getByText('免费版')).toBeVisible()
await expect(page.getByText('专业版')).toBeVisible()
})
})
💡 实用测试技巧
使用 data-testid
// ✅ 推荐 - 使用专门的测试标识
<button data-testid="signup-button">注册</button>
await page.click('[data-testid="signup-button"]')
// ❌ 不推荐 - 依赖文本内容
await page.click('text=注册') // 文本改了测试就坏了
// ❌ 不推荐 - 依赖 CSS 选择器
await page.click('.btn-primary') // 样式改了测试就坏了
等待策略
// 等待元素出现
await expect(page.getByText('加载完成')).toBeVisible()
// 等待 API 请求完成
await page.waitForResponse(response =>
response.url().includes('/api/users') && response.status() === 200
)
// 等待页面跳转
await page.waitForURL('/dashboard')
测试数据管理
// 使用固定测试数据
const testUser = {
email: 'testuser@example.com',
password: 'TestPassword123!',
name: 'Test User'
}
// 或者生成随机数据
const randomEmail = `test${Date.now()}@example.com`
🔧 CI/CD 集成
GitHub Actions 配置
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: bun install
- name: Install Playwright
run: bun --filter web exec playwright install
- name: Run E2E tests
run: bun --filter web e2e:ci
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
环境变量设置
# GitHub Repository > Settings > Secrets
DATABASE_URL=postgresql://test-db-url
NEXTAUTH_SECRET=test-secret-for-ci
STRIPE_SECRET_KEY=sk_test_xxx # 使用测试密钥
测试数据库
# 为 CI 单独准备测试数据库
# 可以使用 GitHub Actions 中的 PostgreSQL service
# 或者专门的测试数据库实例
📊 测试最佳实践
测试分层策略
# 测试金字塔
单元测试 (80%) - 快速,测试单个函数
集成测试 (15%) - 测试模块间协作
E2E 测试 (5%) - 测试用户完整流程
# 重点测试的功能
- 用户注册/登录
- 支付流程
- 核心业务功能
- 关键页面加载
测试用例设计
test.describe('用户注册流程', () => {
test('正常注册', async ({ page }) => {
// 测试正常情况
})
test('邮箱已存在', async ({ page }) => {
// 测试错误情况
})
test('密码不符合要求', async ({ page }) => {
// 测试边界情况
})
})
页面对象模式
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/auth/login')
}
async login(email: string, password: string) {
await this.page.fill('input[name="email"]', email)
await this.page.fill('input[name="password"]', password)
await this.page.click('button[type="submit"]')
}
}
// 在测试中使用
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('test@example.com', 'password')
🆘 常见问题解决
问题 1: 测试偶发失败
// 增加重试次数
test.describe.configure({ retries: 2 })
// 增加等待时间
await page.waitForTimeout(1000)
// 使用更稳定的选择器
await page.waitForSelector('[data-testid="button"]', { state: 'visible' })
问题 2: 测试运行慢
# 并行运行测试
npx playwright test --workers=4
# 只运行指定测试
npx playwright test --grep "注册"
# 使用 headless 模式
npx playwright test --headed=false
问题 3: 调试测试失败
# 打开调试模式
npx playwright test --debug
# 暂停测试执行
await page.pause()
# 截图保存
await page.screenshot({ path: 'debug.png' })
🎯 新人测试策略:何时开始写测试?
初创项目的测试困境
常见担忧:
- "功能还在快速迭代,写了测试也要改"
- "现在都是手工测试,够用了"
- "测试维护成本太高,影响开发速度"
实际情况:这些担忧都有道理,但有更好的平衡策略。
渐进式测试策略
阶段1:核心路径保护(功能稳定后立即开始)
何时开始:核心业务功能(注册/登录/支付)基本定型后
重点测试(基于本项目):
// 最高优先级 - 用户无法使用的致命问题
test.describe('关键业务路径', () => {
test('用户注册登录完整流程', async ({ page }) => {
// src/modules/dashboard/auth/ 相关功能
await page.goto('/auth/signup')
// ... 完整注册流程
await expect(page).toHaveURL('/app')
})
test('项目创建和管理流程', async ({ page }) => {
// src/app/(app)/app/(account)/projects/ 相关功能
await page.goto('/app/projects/create')
// ... 项目创建流程
})
test('事件创建和管理流程', async ({ page }) => {
// src/app/(app)/app/events/ 相关功能
await page.goto('/app/events/create')
// ... 事件管理流程
})
})
阶段2:API稳定性测试(更抗变化)
// API测试比UI测试更稳定,业务逻辑变化少
test.describe('API功能测试', () => {
test('项目CRUD操作', async ({ request }) => {
// 直接测试 src/server/routes/projects.ts
const project = await request.post('/api/projects', {
data: { title: 'Test Project', description: 'Test' }
})
expect(project.ok()).toBeTruthy()
})
test('志愿者角色管理', async ({ request }) => {
// 测试 src/server/routes/volunteer-roles.ts
const roles = await request.get('/api/volunteer-roles')
expect(roles.ok()).toBeTruthy()
})
})
阶段3:回归测试自动化(团队扩大时)
// 防止团队协作时互相破坏功能
test.describe('回归测试', () => {
test('关键页面正常加载', async ({ page }) => {
const pages = ['/', '/projects', '/events', '/about']
for (const url of pages) {
await page.goto(url)
await expect(page.locator('body')).toBeVisible()
}
})
})
减少维护成本的技巧
1. 选择稳定的测试目标
// ✅ 测试稳定的业务流程
test('用户可以创建项目', async ({ page }) => {
// 业务逻辑很少改变
})
// ❌ 避免测试经常变的UI细节
test('按钮是蓝色的', async ({ page }) => {
// 设计改动频繁
})
2. 使用稳定的选择器策略
// ✅ data-testid - 专为测试设计,不会意外改动
<button data-testid="create-project-btn">创建项目</button>
await page.click('[data-testid="create-project-btn"]')
// ⚠️ 业务语义选择器 - 相对稳定
await page.getByRole('button', { name: '创建项目' })
// ❌ CSS类选择器 - 样式重构时容易坏
await page.click('.btn-primary.create-btn')
3. 合理的测试数据策略
// 测试前重置数据,避免数据污染
test.beforeEach(async () => {
// 清理测试数据或使用独立测试数据库
await resetTestData()
})
// 使用确定性测试数据
const testUser = {
email: 'test@example.com',
password: 'Test123!',
name: 'Test User'
}
实际成本收益分析
投入成本
- 初期设置:1-2周(配置环境、写核心测试)
- 日常维护:每个新功能额外20%开发时间
- 学习成本:团队成员1-2天掌握基础
收益回报
- 手工测试时间:从每次发布2小时减少到10分钟
- 线上故障:减少90%因功能回归导致的用户问题
- 重构信心:敢于优化代码,不怕破坏功能
- 团队协作:多人开发时互不影响
量化指标(基于类似项目经验)
# 6个月后的对比
手工测试项目:
- 每次发布: 2小时测试 + 1小时修复问题
- 月发布4次: 12小时/月
- 线上故障: 2次/月,每次影响50用户
自动化测试项目:
- 每次发布: 10分钟运行测试
- 月发布8次: 1.3小时/月(可以更频繁发布)
- 线上故障: 0.2次/月,影响范围更小
项目具体实施计划
Week 1: 基础设施
# 1. 创建基本测试配置
touch playwright.config.ts
mkdir tests
# 2. 添加测试脚本到关键组件
# 在 src/modules/dashboard/auth/components/ 中添加 data-testid
# 在 src/modules/dashboard/profile/components/ 中添加 data-testid
Week 2: 核心路径测试
// tests/auth.spec.ts - 用户认证流程
// tests/projects.spec.ts - 项目管理流程
// tests/events.spec.ts - 事件管理流程
Week 3-4: API和边界测试
// tests/api.spec.ts - API功能测试
// tests/edge-cases.spec.ts - 错误处理测试
给新人的建议
- 从小开始:先写3-5个最核心的测试,建立信心
- 优先级明确:用户无法使用的功能 > 数据损坏的功能 > UI细节
- 渐进改善:每次发现手工测试的重复工作,就考虑自动化
- 团队共识:确保团队理解测试的价值,愿意维护测试代码
判断测试ROI的标准
// 值得写测试的功能特征:
// ✅ 用户高频使用(如登录、创建项目)
// ✅ 出错影响大(如支付、数据保存)
// ✅ 逻辑复杂(如权限判断、状态流转)
// ✅ 回归风险高(如核心业务流程)
// 暂时不写测试的功能:
// ❌ 纯展示页面(如关于我们、帮助页面)
// ❌ 频繁变化的UI(如样式调整、布局实验)
// ❌ 一次性功能(如数据迁移脚本)
📎 延伸阅读
官方文档:
- Playwright 官方文档 - 最全面的使用指南
- Playwright Best Practices - 官方最佳实践
学习资源:
- Test Automation University - 免费测试自动化课程
- Playwright 中文教程 - 官方中文文档
工具推荐:
- Playwright VS Code Extension - VS Code 插件
- Playwright Trace Viewer - 测试执行轨迹查看器
下一步: 完成测试设置后,查看 技术栈说明 了解整体架构设计。