|
| 1 | +# Jest 单元测试文档 |
| 2 | + |
| 3 | +## ✅ 测试结果 |
| 4 | + |
| 5 | +``` |
| 6 | +✅ Test Suites: 3 passed, 3 total |
| 7 | +✅ Tests: 46 passed, 46 total |
| 8 | +⏱️ Time: ~50s |
| 9 | +``` |
| 10 | + |
| 11 | +## 📁 项目结构(源文件和测试文件分离) |
| 12 | + |
| 13 | +``` |
| 14 | +front/ |
| 15 | +├── __mocks__/ ← Mock文件目录 |
| 16 | +│ ├── fileMock.js ← 静态文件Mock |
| 17 | +│ └── next-navigation.ts ← Next.js路由Mock |
| 18 | +│ |
| 19 | +├── __tests__/ ← 测试文件目录(和app平级)✨ |
| 20 | +│ ├── bind_phone/ |
| 21 | +│ │ └── normalForm.test.tsx ← 绑定手机号登录测试(10个测试) |
| 22 | +│ └── register/ |
| 23 | +│ ├── captcha.test.tsx ← 验证码组件测试(14个测试) |
| 24 | +│ └── phone.test.tsx ← 注册页面测试(22个测试) |
| 25 | +│ |
| 26 | +├── app/ ← 源代码目录 |
| 27 | +│ ├── bind_phone/ |
| 28 | +│ │ ├── normalForm.tsx ← 绑定手机号登录源文件 |
| 29 | +│ │ ├── page.tsx |
| 30 | +│ │ └── page.module.scss |
| 31 | +│ └── register/ |
| 32 | +│ ├── captcha.tsx ← 验证码组件源文件 |
| 33 | +│ ├── phone.tsx ← 注册页面源文件 |
| 34 | +│ └── ... |
| 35 | +│ |
| 36 | +├── infrastructure/ ← API和基础设施 |
| 37 | +├── shared/ ← 共享工具 |
| 38 | +├── jest.config.js ← Jest配置文件 |
| 39 | +├── jest.setup.js ← Jest环境设置 |
| 40 | +└── test-utils.tsx ← 测试工具函数 |
| 41 | +``` |
| 42 | + |
| 43 | +## 🎯 关键特点 |
| 44 | + |
| 45 | +### ✨ 源文件和测试文件完全分离 |
| 46 | + |
| 47 | +- **源文件**: `app/` 目录下 |
| 48 | +- **测试文件**: `__tests__/` 目录下(和 `app/` 平级) |
| 49 | +- **Mock文件**: `__mocks__/` 目录下(统一管理) |
| 50 | + |
| 51 | +### 🔗 目录镜像关系 |
| 52 | + |
| 53 | +| 源文件 | 测试文件 | |
| 54 | +|--------|---------| |
| 55 | +| `app/bind_phone/normalForm.tsx` | `__tests__/bind_phone/normalForm.test.tsx` | |
| 56 | +| `app/register/captcha.tsx` | `__tests__/register/captcha.test.tsx` | |
| 57 | +| `app/register/phone.tsx` | `__tests__/register/phone.test.tsx` | |
| 58 | + |
| 59 | +## 📊 测试覆盖详情 |
| 60 | + |
| 61 | +### 1. normalForm.test.tsx - 绑定手机号登录 (10个测试) |
| 62 | + |
| 63 | +✅ 渲染测试 |
| 64 | +- 正确渲染所有表单元素 |
| 65 | +- 手机号输入框限制11位 |
| 66 | + |
| 67 | +✅ 表单验证测试 |
| 68 | +- 空表单验证错误 |
| 69 | +- 无效手机号格式错误 |
| 70 | + |
| 71 | +✅ 功能测试 |
| 72 | +- 成功提交流程(API调用、保存token、页面跳转) |
| 73 | +- 登录失败处理 |
| 74 | +- Loading状态显示 |
| 75 | +- 参数验证(openid/provider) |
| 76 | +- 验证码功能集成 |
| 77 | + |
| 78 | +### 2. captcha.test.tsx - 验证码组件 (14个测试) |
| 79 | + |
| 80 | +✅ 渲染测试 |
| 81 | +- 正确渲染输入框和按钮 |
| 82 | + |
| 83 | +✅ 交互测试 |
| 84 | +- 用户输入 |
| 85 | +- 按钮点击 |
| 86 | + |
| 87 | +✅ 倒计时功能 |
| 88 | +- 开始倒计时 |
| 89 | +- 倒计时期间按钮禁用 |
| 90 | +- 倒计时结束恢复 |
| 91 | + |
| 92 | +✅ 状态测试 |
| 93 | +- Loading状态 |
| 94 | +- 错误状态显示 |
| 95 | +- 错误清除 |
| 96 | + |
| 97 | +✅ 配置测试 |
| 98 | +- 自定义倒计时秒数 |
| 99 | +- 自定义样式 |
| 100 | +- Label和Required支持 |
| 101 | + |
| 102 | +### 3. phone.test.tsx - 注册页面 (22个测试) |
| 103 | + |
| 104 | +✅ 渲染测试 |
| 105 | +- 所有必填字段正确渲染 |
| 106 | +- 输入框限制和属性 |
| 107 | + |
| 108 | +✅ 表单验证测试 |
| 109 | +- 用户名:30位以内英文数字 |
| 110 | +- 邮箱:格式验证 |
| 111 | +- 密码: |
| 112 | + - 长度8-30位 |
| 113 | + - 必须包含小写字母 |
| 114 | + - 必须包含大写字母 |
| 115 | + - 必须包含数字 |
| 116 | + - 必须包含特殊符号 |
| 117 | +- 确认密码:与密码一致 |
| 118 | +- 手机号:中国大陆格式 |
| 119 | + |
| 120 | +✅ 功能测试 |
| 121 | +- 成功注册流程(加密、API调用、消息提示、跳转) |
| 122 | +- 注册失败处理 |
| 123 | +- Loading状态 |
| 124 | +- URL参数自动填充 |
| 125 | +- 验证码集成 |
| 126 | + |
| 127 | +## 🚀 使用方法 |
| 128 | + |
| 129 | +### 运行所有测试 |
| 130 | +```bash |
| 131 | +cd front |
| 132 | +npm test |
| 133 | +``` |
| 134 | + |
| 135 | +### 监听模式(开发推荐) |
| 136 | +```bash |
| 137 | +npm run test:watch |
| 138 | +``` |
| 139 | + |
| 140 | +### 生成覆盖率报告 |
| 141 | +```bash |
| 142 | +npm run test:coverage |
| 143 | +``` |
| 144 | + |
| 145 | +### CI环境运行 |
| 146 | +```bash |
| 147 | +npm run test:ci |
| 148 | +``` |
| 149 | + |
| 150 | +## 🔧 测试配置说明 |
| 151 | + |
| 152 | +### jest.config.js |
| 153 | +```javascript |
| 154 | +{ |
| 155 | + testEnvironment: 'jest-environment-jsdom', // 浏览器环境 |
| 156 | + testMatch: [ |
| 157 | + '**/__tests__/**/*.[jt]s?(x)', // 匹配__tests__目录 |
| 158 | + ], |
| 159 | + moduleNameMapper: { |
| 160 | + '^@/(.*)$': '<rootDir>/$1', // @别名支持 |
| 161 | + }, |
| 162 | +} |
| 163 | +``` |
| 164 | + |
| 165 | +### jest.setup.js |
| 166 | +- Mock浏览器API(IntersectionObserver, ResizeObserver, matchMedia) |
| 167 | +- Mock localStorage |
| 168 | +- 过滤无用的警告信息 |
| 169 | + |
| 170 | +## 📝 编写新测试 |
| 171 | + |
| 172 | +### 步骤1:确定源文件位置 |
| 173 | +``` |
| 174 | +app/auth/login/LoginForm.tsx |
| 175 | +``` |
| 176 | + |
| 177 | +### 步骤2:在__tests__下创建镜像目录 |
| 178 | +``` |
| 179 | +__tests__/auth/login/LoginForm.test.tsx |
| 180 | +``` |
| 181 | + |
| 182 | +### 步骤3:使用@别名导入源文件 |
| 183 | +```typescript |
| 184 | +import LoginForm from '@/app/auth/login/LoginForm' |
| 185 | +``` |
| 186 | + |
| 187 | +### 步骤4:编写测试 |
| 188 | +```typescript |
| 189 | +describe('LoginForm', () => { |
| 190 | + test('应该正确渲染', () => { |
| 191 | + render(<LoginForm />) |
| 192 | + expect(screen.getByText('登录')).toBeInTheDocument() |
| 193 | + }) |
| 194 | +}) |
| 195 | +``` |
| 196 | + |
| 197 | +## 🎨 测试模式 |
| 198 | + |
| 199 | +### 模式1:AAA模式(Arrange-Act-Assert) |
| 200 | + |
| 201 | +```typescript |
| 202 | +test('用户登录成功', async () => { |
| 203 | + // Arrange - 准备 |
| 204 | + const user = userEvent.setup() |
| 205 | + render(<LoginForm />) |
| 206 | + |
| 207 | + // Act - 执行 |
| 208 | + await user.type(screen.getByPlaceholderText('用户名'), 'testuser') |
| 209 | + await user.click(screen.getByRole('button', { name: '登录' })) |
| 210 | + |
| 211 | + // Assert - 断言 |
| 212 | + await waitFor(() => { |
| 213 | + expect(mockLogin).toHaveBeenCalled() |
| 214 | + }) |
| 215 | +}) |
| 216 | +``` |
| 217 | + |
| 218 | +### 模式2:Mock外部依赖 |
| 219 | + |
| 220 | +```typescript |
| 221 | +// Mock API |
| 222 | +jest.mock('@/infrastructure/api/common', () => ({ |
| 223 | + login: jest.fn(), |
| 224 | +})) |
| 225 | + |
| 226 | +// Mock Next.js路由 |
| 227 | +jest.mock('next/navigation', () => ({ |
| 228 | + useRouter: jest.fn(), |
| 229 | +})) |
| 230 | +``` |
| 231 | + |
| 232 | +### 模式3:测试异步操作 |
| 233 | + |
| 234 | +```typescript |
| 235 | +test('异步操作', async () => { |
| 236 | + await user.click(button) |
| 237 | + |
| 238 | + await waitFor(() => { |
| 239 | + expect(screen.getByText('成功')).toBeInTheDocument() |
| 240 | + }) |
| 241 | +}) |
| 242 | +``` |
| 243 | + |
| 244 | +## 💡 最佳实践 |
| 245 | + |
| 246 | +### ✅ DO - 推荐做法 |
| 247 | + |
| 248 | +1. **测试用户行为,而非实现细节** |
| 249 | +```typescript |
| 250 | +// ✅ 好 |
| 251 | +expect(screen.getByText('登录成功')).toBeInTheDocument() |
| 252 | + |
| 253 | +// ❌ 不好 |
| 254 | +expect(component.state.isLoggedIn).toBe(true) |
| 255 | +``` |
| 256 | + |
| 257 | +2. **使用语义化查询** |
| 258 | +```typescript |
| 259 | +// ✅ 好 |
| 260 | +screen.getByRole('button', { name: '登录' }) |
| 261 | + |
| 262 | +// ❌ 不好 |
| 263 | +screen.getByTestId('login-button') |
| 264 | +``` |
| 265 | + |
| 266 | +3. **每个测试只测一件事** |
| 267 | +```typescript |
| 268 | +// ✅ 好 |
| 269 | +test('应该显示错误消息', () => {}) |
| 270 | +test('应该禁用按钮', () => {}) |
| 271 | + |
| 272 | +// ❌ 不好 |
| 273 | +test('表单功能', () => { |
| 274 | + // 测试渲染、验证、提交、错误... |
| 275 | +}) |
| 276 | +``` |
| 277 | + |
| 278 | +4. **保持测试独立** |
| 279 | +```typescript |
| 280 | +beforeEach(() => { |
| 281 | + jest.clearAllMocks() // 每个测试前清理 |
| 282 | +}) |
| 283 | +``` |
| 284 | + |
| 285 | +### ❌ DON'T - 避免的做法 |
| 286 | + |
| 287 | +1. ❌ 不要测试第三方库的功能 |
| 288 | +2. ❌ 不要在测试中使用真实的API |
| 289 | +3. ❌ 不要让测试互相依赖 |
| 290 | +4. ❌ 不要忽略异步操作 |
| 291 | + |
| 292 | +## 🔍 常见问题 |
| 293 | + |
| 294 | +### Q1: 测试文件放在哪里? |
| 295 | +**A**: 放在 `front/__tests__/` 目录下,与 `app/` 平级,保持目录镜像结构。 |
| 296 | + |
| 297 | +### Q2: 如何导入源文件? |
| 298 | +**A**: 使用@别名:`import Component from '@/app/path/to/Component'` |
| 299 | + |
| 300 | +### Q3: 如何Mock API? |
| 301 | +**A**: 使用 `jest.mock('@/infrastructure/api/common', () => ({...}))` |
| 302 | + |
| 303 | +### Q4: 如何测试异步操作? |
| 304 | +**A**: 使用 `await waitFor(() => { expect(...) })` |
| 305 | + |
| 306 | +### Q5: 如何处理警告? |
| 307 | +**A**: 在 `jest.setup.js` 中过滤,已配置常见警告过滤。 |
| 308 | + |
| 309 | +## 📚 测试文件说明 |
| 310 | + |
| 311 | +### normalForm.test.tsx |
| 312 | +测试**绑定手机号登录**功能: |
| 313 | +- 表单渲染和验证 |
| 314 | +- 登录成功/失败流程 |
| 315 | +- OAuth参数处理 |
| 316 | +- 验证码集成 |
| 317 | + |
| 318 | +### phone.test.tsx |
| 319 | +测试**用户注册**功能: |
| 320 | +- 所有表单字段验证 |
| 321 | +- 密码复杂度要求 |
| 322 | +- 注册成功/失败流程 |
| 323 | +- 数据加密 |
| 324 | +- 消息提示 |
| 325 | + |
| 326 | +### captcha.test.tsx |
| 327 | +测试**验证码组件**功能: |
| 328 | +- 倒计时逻辑 |
| 329 | +- 按钮状态管理 |
| 330 | +- 错误处理 |
| 331 | +- 自定义配置 |
| 332 | + |
| 333 | +## 🎓 学习资源 |
| 334 | + |
| 335 | +- [Jest官方文档](https://jestjs.io/) |
| 336 | +- [React Testing Library](https://testing-library.com/react) |
| 337 | +- [Testing Library 最佳实践](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library) |
| 338 | + |
| 339 | +## 📦 依赖包 |
| 340 | + |
| 341 | +```json |
| 342 | +{ |
| 343 | + "devDependencies": { |
| 344 | + "jest": "^30.2.0", |
| 345 | + "@testing-library/react": "latest", |
| 346 | + "@testing-library/jest-dom": "latest", |
| 347 | + "@testing-library/user-event": "latest", |
| 348 | + "jest-environment-jsdom": "latest", |
| 349 | + "@swc/jest": "latest", |
| 350 | + "identity-obj-proxy": "latest" |
| 351 | + } |
| 352 | +} |
| 353 | +``` |
| 354 | + |
| 355 | +## ✨ 总结 |
| 356 | + |
| 357 | +**完整的测试环境已配置完成!** |
| 358 | + |
| 359 | +- ✅ 46个测试全部通过 |
| 360 | +- ✅ 源文件和测试文件完全分离 |
| 361 | +- ✅ 测试文件在 `front/__tests__/` 下(和 `app/` 平级) |
| 362 | +- ✅ 清晰的目录结构 |
| 363 | +- ✅ 完善的测试覆盖 |
| 364 | + |
| 365 | +**项目结构清晰,易于维护!** 🚀 |
| 366 | + |
| 367 | +--- |
| 368 | + |
| 369 | +**最后更新**: 2024-11-18 |
| 370 | +**测试状态**: ✅ 全部通过 |
| 371 | + |
0 commit comments