Day 10: 测试驱动
编写测试保证代码质量,让重构更安全、发布更自信。
测试基础概念
测试金字塔
你:解释一下测试金字塔,什么时候写单元测试、集成测试、E2E测试
测试层级(从下到上):
- 单元测试:测试单个函数/组件
- 集成测试:测试模块间交互
- E2E测试:测试完整用户流程
测试驱动开发(TDD)
你:用 TDD 方式开发一个计算器函数
TDD 流程:
- 写测试(红色)
- 写代码让测试通过(绿色)
- 重构代码(重构)
单元测试
React 组件测试
你:为 Button 组件写单元测试,测试点击事件和不同状态
import { render, fireEvent, screen } from '@testing-library/react';
import Button from './Button';
test('按钮点击事件', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>点击我</Button>);
fireEvent.click(screen.getByText('点击我'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
test('禁用状态', () => {
render(<Button disabled>禁用按钮</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
工具函数测试
你:为日期格式化函数写测试,覆盖各种输入情况
Mock 数据和函数
你:测试用户登录函数,Mock 掉 API 调用
集成测试
API 测试
你:为用户注册 API 写集成测试,包含成功和失败场景
const request = require('supertest');
const app = require('../app');
describe('用户注册 API', () => {
test('注册成功', async () => {
const response = await request(app)
.post('/api/users/register')
.send({
email: 'test@example.com',
password: 'password123',
name: '测试用户'
});
expect(response.status).toBe(201);
expect(response.body.user.email).toBe('test@example.com');
});
test('邮箱重复注册', async () => {
await request(app)
.post('/api/users/register')
.send({
email: 'duplicate@example.com',
password: 'password123',
name: '用户1'
});
const response = await request(app)
.post('/api/users/register')
.send({
email: 'duplicate@example.com',
password: 'password456',
name: '用户2'
});
expect(response.status).toBe(409);
expect(response.body.error).toContain('邮箱已存在');
});
});
数据库测试
你:测试用户 CRUD 操作,每个测试前后清理数据库
E2E 测试
Playwright 测试
你:用 Playwright 测试完整的用户注册登录流程
const { test, expect } = require('@playwright/test');
test('用户注册登录流程', async ({ page }) => {
// 访问注册页面
await page.goto('/register');
// 填写注册表单
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.fill('input[name="name"]', '测试用户');
// 点击注册按钮
await page.click('button[type="submit"]');
// 验证跳转到登录页
await expect(page).toHaveURL('/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.locator('.welcome-message')).toContainText('欢迎,测试用户');
});
页面截图对比
你:添加视觉回归测试,对比页面截图发现 UI 变化
测试配置
Jest 配置
你:配置 Jest 测试环境,支持 ES6 模块和 React 组件测试
测试覆盖率
你:配置测试覆盖率报告,要求覆盖率达到 80%
测试数据管理
你:创建测试数据工厂,生成各种测试用的用户数据
常见测试场景
表单验证测试
你:测试登录表单的各种验证规则
- 邮箱格式验证
- 密码长度验证
- 必填字段验证
- 提交按钮状态
异步操作测试
你:测试数据获取组件的加载、成功、错误状态
权限测试
你:测试不同用户角色的页面访问权限
支付流程测试
你:测试完整的订单支付流程,包含各种异常情况
测试工具选择
前端测试工具
- Jest:JavaScript 测试框架
- React Testing Library:React 组件测试
- Playwright:E2E 测试
- Storybook:组件测试和文档
后端测试工具
- Jest:单元测试
- Supertest:API 测试
- Sinon:Mock 和 Stub
- Artillery:性能测试
测试最佳实践
测试命名
// ❌ 不好的测试名
test('test user function', () => {});
// ✅ 好的测试名
test('用户登录失败时显示错误信息', () => {});
测试结构
// AAA 模式:Arrange, Act, Assert
test('计算总价包含税费', () => {
// Arrange - 准备数据
const items = [{ price: 100 }, { price: 200 }];
const taxRate = 0.1;
// Act - 执行操作
const total = calculateTotal(items, taxRate);
// Assert - 断言结果
expect(total).toBe(330);
});
测试隔离
你:确保每个测试相互独立,不依赖其他测试的结果
测试驱动开发实战
开发新功能
你:用 TDD 开发一个购物车功能
步骤:
- 写测试:添加商品到购物车
- 写代码:实现添加功能
- 重构:优化代码结构
- 写测试:删除购物车商品
- 写代码:实现删除功能
- 重构:继续优化
Bug 修复
你:先写测试重现 bug,然后修复让测试通过
持续集成测试
GitHub Actions
你:配置 CI 流水线,每次 Push 代码自动运行测试
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- run: npm install
- run: npm test
- run: npm run test:e2e
测试结果通知
你:测试失败时自动发送邮件通知团队
性能测试
压力测试
你:测试 API 在高并发下的表现,找出性能瓶颈
内存泄漏测试
你:检查长时间运行的程序是否有内存泄漏
测试理念:测试不是为了证明程序没有 bug,而是为了快速发现 bug。好的测试是最好的文档。
第十天完成! 你已经学会用测试保证代码质量了。明天学习部署上线。
下一步
Day 11: 部署上线 - 将应用部署到生产环境
练习建议:
- 为现有项目添加测试,体验测试带来的安全感
- 试试 TDD 开发一个小功能
- 配置测试覆盖率,看看哪些代码没有测试到