文档
开发指南测试指南

测试指南

🚀 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 - 错误处理测试

给新人的建议

  1. 从小开始:先写3-5个最核心的测试,建立信心
  2. 优先级明确:用户无法使用的功能 > 数据损坏的功能 > UI细节
  3. 渐进改善:每次发现手工测试的重复工作,就考虑自动化
  4. 团队共识:确保团队理解测试的价值,愿意维护测试代码

判断测试ROI的标准

// 值得写测试的功能特征:
// ✅ 用户高频使用(如登录、创建项目)
// ✅ 出错影响大(如支付、数据保存)
// ✅ 逻辑复杂(如权限判断、状态流转)
// ✅ 回归风险高(如核心业务流程)

// 暂时不写测试的功能:
// ❌ 纯展示页面(如关于我们、帮助页面)  
// ❌ 频繁变化的UI(如样式调整、布局实验)
// ❌ 一次性功能(如数据迁移脚本)

📎 延伸阅读

官方文档:

学习资源:

工具推荐:


下一步: 完成测试设置后,查看 技术栈说明 了解整体架构设计。