Parcourir la source

📝 docs(architecture): 新增 Mini UI 包测试规范文档

- 创建 mini-ui-testing-standards.md 文档,基于故事 017.003 的实施经验总结
- 规定 Mini UI 包测试编写规范,确保测试的一致性、可维护性和正确性
- 明确核心原则:使用 Jest 而非 Vitest,使用共享的 mini-testing-utils,使用真实的 React Query
- 提供 Jest 配置规范、测试文件结构规范、测试编写规范、测试最佳实践和常见错误示例
- 包含测试执行命令和 mini-testing-utils 扩展指南

✅ test(rencai-personal-info-ui): 清理 PersonalInfoPage 测试文件中的冗余声明

- 移除测试文件中重复的全局类型声明(describe, it, expect, jest, beforeEach)
- 这些声明已在 mini-testing-utils 的共享 setup 文件中提供
yourname il y a 3 semaines
Parent
commit
72fe5cd848

+ 491 - 0
docs/architecture/mini-ui-testing-standards.md

@@ -0,0 +1,491 @@
+# Mini UI 包测试规范
+
+**版本**: 1.0
+**创建日期**: 2025-12-26
+**适用范围**: 所有 Taro 小程序 UI 包测试
+
+## 概述
+
+本文档规定了 Mini UI 包的测试编写规范,基于故事 017.003 的实施经验总结。遵循这些规范可以确保测试的一致性、可维护性和正确性。
+
+## 核心原则
+
+### 1. 使用 Jest,不是 Vitest
+
+**重要**: Mini UI 包使用 **Jest** 测试框架,不是 Vitest。
+
+```javascript
+// jest.config.cjs
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'jsdom',
+  // ...
+}
+```
+
+### 2. 使用共享的 mini-testing-utils
+
+**关键原则**: 不要在每个 UI 包中重写 Taro mock,直接使用 `@d8d/mini-testing-utils` 提供的共享 mock。
+
+```javascript
+// jest.config.cjs
+module.exports = {
+  setupFilesAfterEnv: ['@d8d/mini-testing-utils/testing/setup'],
+  moduleNameMapper: {
+    '^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',
+  }
+}
+```
+
+### 3. 使用真实的 React Query
+
+**关键原则**: 使用真实的 React Query(不要 mock),以便验证 RPC 类型推断。
+
+```typescript
+// ✅ 正确: 使用真实的 React Query
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+
+const createTestWrapper = () => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: { retry: false, staleTime: Infinity }
+    }
+  })
+  return ({ children }: { children: React.ReactNode }) => (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  )
+}
+```
+
+```typescript
+// ❌ 错误: 不要 mock React Query
+jest.mock('@tanstack/react-query', () => ({
+  useQuery: jest.fn()
+}))
+```
+
+## Jest 配置规范
+
+### 标准 jest.config.cjs 模板
+
+```javascript
+// jest.config.cjs
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'jsdom',
+
+  // 使用 mini-testing-utils 提供的共享 setup
+  setupFilesAfterEnv: ['@d8d/mini-testing-utils/testing/setup'],
+
+  moduleNameMapper: {
+    // 测试文件中的别名映射(仅用于测试文件)
+    '^@/(.*)$': '<rootDir>/src/$1',
+    '^~/(.*)$': '<rootDir>/tests/$1',
+
+    // Taro API 重定向到共享 mock
+    '^@tarojs/taro$': '@d8d/mini-testing-utils/testing/taro-api-mock.ts',
+
+    // 样式和文件映射
+    '\\.(css|less|scss|sass)$': '@d8d/mini-testing-utils/testing/style-mock.js',
+    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
+      '@d8d/mini-testing-utils/testing/file-mock.js'
+  },
+
+  testMatch: [
+    '<rootDir>/tests/**/*.spec.{ts,tsx}',
+    '<rootDir>/tests/**/*.test.{ts,tsx}'
+  ],
+
+  collectCoverageFrom: [
+    'src/**/*.{ts,tsx}',
+    '!src/**/*.d.ts',
+    '!src/**/index.{ts,tsx}',
+    '!src/**/*.stories.{ts,tsx}'
+  ],
+
+  coverageDirectory: 'coverage',
+  coverageReporters: ['text', 'lcov', 'html'],
+
+  testPathIgnorePatterns: [
+    '/node_modules/',
+    '/dist/',
+    '/coverage/'
+  ],
+
+  transform: {
+    '^.+\\.(ts|tsx)$': 'ts-jest',
+    '^.+\\.(js|jsx)$': 'babel-jest'
+  },
+
+  transformIgnorePatterns: [
+    '/node_modules/(?!(swiper|@tarojs)/)'
+  ],
+
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
+}
+```
+
+## 测试文件结构规范
+
+```
+mini-ui-packages/<package-name>/
+└── tests/
+    ├── unit/                      # 单元测试
+    │   └── components/
+    │       ├── ComponentName.test.tsx
+    │       └── ...
+    └── pages/                     # 页面组件测试
+        └── PageName/
+            └── PageName.test.tsx
+```
+
+## 测试编写规范
+
+### 1. 组件测试模板
+
+```typescript
+/**
+ * ComponentName 组件测试
+ */
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import ComponentName, { PropsInterface } from '../../../src/components/ComponentName'
+import Taro from '@tarojs/taro'
+
+declare const describe: any
+declare const it: any
+declare const expect: any
+declare const jest: any
+declare const beforeEach: any
+
+// 使用 mini-testing-utils 提供的 Taro mock
+// jest.mock('@tarojs/taro') 在 testing/setup.ts 中已配置
+
+describe('ComponentName', () => {
+  const mockProps: PropsInterface = {
+    // mock props
+  }
+
+  beforeEach(() => {
+    // 清理 Taro API mock
+    ;(Taro.someApi as jest.Mock).mockClear()
+  })
+
+  it('should render correctly', () => {
+    render(<ComponentName {...mockProps} />)
+    expect(screen.getByText('expected text')).toBeInTheDocument()
+  })
+})
+```
+
+### 2. 页面组件测试模板
+
+```typescript
+/**
+ * PageName 页面测试
+ * 使用真实的React Query和RPC类型验证
+ */
+import React from 'react'
+import { render, screen, waitFor } from '@testing-library/react'
+import '@testing-library/jest-dom'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import PageName from '../../../src/pages/PageName/PageName'
+import { apiClient } from '../../../src/api'
+import { useRequireAuth } from '@d8d/xxx-auth-ui/hooks'
+import Taro from '@tarojs/taro'
+
+declare const describe: any
+declare const it: any
+declare const expect: any
+declare const jest: any
+declare const beforeEach: any
+
+// 使用 mini-testing-utils 提供的 Taro mock
+// jest.mock('@tarojs/taro') 在 testing/setup.ts 中已配置
+
+// Mock auth hooks
+jest.mock('@d8d/xxx-auth-ui/hooks', () => ({
+  useRequireAuth: jest.fn()
+}))
+
+// Mock API client - 使用真实的RPC类型
+jest.mock('../../../src/api', () => ({
+  apiClient: {
+    route: {
+      $get: jest.fn()
+    }
+  }
+}))
+
+const createMockResponse = <T,>(status: number, data: T) => ({
+  status,
+  ok: status >= 200 && status < 300,
+  json: async () => data
+})
+
+const createTestWrapper = () => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+        staleTime: Infinity,
+        refetchOnWindowFocus: false
+      }
+    }
+  })
+
+  return ({ children }: { children: React.ReactNode }) => (
+    <QueryClientProvider client={queryClient}>
+      {children}
+    </QueryClientProvider>
+  )
+}
+
+describe('PageName', () => {
+  beforeEach(() => {
+    jest.clearAllMocks()
+    // Mock useRequireAuth to do nothing (user is authenticated)
+    ;(useRequireAuth as jest.Mock).mockImplementation(() => {})
+    // Reset Taro API mocks
+    ;(Taro.setNavigationBarTitle as jest.Mock).mockClear()
+    ;(Taro.showToast as jest.Mock).mockClear()
+  })
+
+  it('should render page with correct data', async () => {
+    // Mock API calls - 使用符合RPC类型的响应
+    ;(apiClient.route.$get as jest.Mock).mockResolvedValue(
+      createMockResponse(200, mockData)
+    )
+
+    const wrapper = createTestWrapper()
+    render(<PageName />, { wrapper })
+
+    await waitFor(() => {
+      expect(screen.getByText('expected text')).toBeInTheDocument()
+    })
+  })
+})
+```
+
+### 3. Mock RPC 响应规范
+
+```typescript
+// ✅ 正确: 使用 Mock 响应工具函数
+const createMockResponse = <T,>(status: number, data: T) => ({
+  status,
+  ok: status >= 200 && status < 300,
+  json: async () => data
+})
+
+// 使用示例
+; (apiClient.route.$get as jest.Mock).mockResolvedValue(
+  createMockResponse(200, { name: '张三', age: 35 })
+)
+```
+
+### 4. Taro API 使用规范
+
+```typescript
+// ✅ 正确: 直接使用 Taro API,不需要自定义 mock
+import Taro from '@tarojs/taro'
+
+describe('Component', () => {
+  beforeEach(() => {
+    // 清理 mock
+    ;(Taro.showToast as jest.Mock).mockClear()
+  })
+
+  it('should call Taro API', () => {
+    render(<Component />)
+    expect(Taro.showToast).toHaveBeenCalledWith({ title: '成功' })
+  })
+})
+
+// ❌ 错误: 不要在测试文件中自定义 Taro mock
+jest.mock('@tarojs/taro', () => ({
+  default: {
+    showToast: jest.fn()  // 错误!应该使用 mini-testing-utils 的共享 mock
+  }
+}))
+```
+
+## 测试最佳实践
+
+### 1. RPC 类型推断验证
+
+使用真实的 React Query 来验证 RPC 类型推断:
+
+```typescript
+// ✅ 正确: 测试中使用真实的 RPC 类型
+const mockData = {
+  name: '张三',
+  age: 35,
+  // 类型错误会在编译时被捕获
+  wrongField: 'type error'  // TypeScript 会报错
+}
+
+; (apiClient.route.$get as jest.Mock).mockResolvedValue(
+  createMockResponse(200, mockData)
+)
+```
+
+### 2. 异步测试
+
+使用 `waitFor` 处理异步状态更新:
+
+```typescript
+it('should render data after loading', async () => {
+  ; (apiClient.route.$get as jest.Mock).mockResolvedValue(
+    createMockResponse(200, mockData)
+  )
+
+  render(<Page />, { wrapper })
+
+  await waitFor(() => {
+    expect(screen.getByText('data')).toBeInTheDocument()
+  })
+})
+```
+
+### 3. 用户交互测试
+
+```typescript
+it('should handle click event', () => {
+  const { fireEvent } = require('@testing-library/react')
+
+  render(<Component />)
+
+  const button = screen.getByText('Click me')
+  fireEvent.click(button)
+
+  expect(Taro.someApi).toHaveBeenCalled()
+})
+```
+
+## 常见错误
+
+### 错误 1: 重写 Taro mock
+
+```typescript
+// ❌ 错误: 在测试文件中重写 Taro mock
+jest.mock('@tarojs/taro', () => ({
+  default: {
+    showToast: jest.fn()
+  }
+}))
+
+// ✅ 正确: 直接导入使用
+import Taro from '@tarojs/taro'
+// mock 在 mini-testing-utils/testing/setup.ts 中已配置
+```
+
+### 错误 2: Mock React Query
+
+```typescript
+// ❌ 错误: Mock React Query
+jest.mock('@tanstack/react-query', () => ({
+  useQuery: jest.fn(() => ({ data: mockData }))
+}))
+
+// ✅ 正确: 使用真实的 React Query
+const wrapper = createTestWrapper()
+render(<Page />, { wrapper })
+```
+
+### 错误 3: 在 UI 包源代码中使用别名
+
+```typescript
+// ❌ 错误: 源代码中使用别名
+import { apiClient } from '@/api'
+import { Component } from '@/components'
+
+// ✅ 正确: 源代码中使用相对路径
+import { apiClient } from '../api'
+import { Component } from '../components'
+```
+
+### 错误 4: 忘记清理 mock
+
+```typescript
+// ❌ 错误: 忘记清理 mock
+describe('Component', () => {
+  it('test 1', () => {
+    expect(Taro.showToast).toHaveBeenCalledTimes(1)
+  })
+
+  it('test 2', () => {
+    // test 1 的 mock 调用会影响 test 2
+    expect(Taro.showToast).toHaveBeenCalledTimes(1)  // 可能失败
+  })
+})
+
+// ✅ 正确: 在 beforeEach 中清理 mock
+describe('Component', () => {
+  beforeEach(() => {
+    ; (Taro.showToast as jest.Mock).mockClear()
+  })
+
+  it('test 1', () => {
+    expect(Taro.showToast).toHaveBeenCalledTimes(1)
+  })
+
+  it('test 2', () => {
+    // mock 已清理,计数重新开始
+    expect(Taro.showToast).toHaveBeenCalledTimes(1)
+  })
+})
+```
+
+## 测试执行
+
+```bash
+# 运行所有测试
+cd mini-ui-packages/<package-name> && pnpm test
+
+# 运行特定测试
+pnpm test --testNamePattern="ComponentName"
+
+# 生成覆盖率报告
+pnpm test:coverage
+
+# 监听模式
+pnpm test:watch
+```
+
+## mini-testing-utils 扩展
+
+如果需要添加新的 Taro API mock,在 `mini-testing-utils/testing/taro-api-mock.ts` 中添加:
+
+```typescript
+// 1. 添加 mock 函数声明
+export const mockNewApi = jest.fn()
+
+// 2. 在默认导出中添加
+export default {
+  // ...
+  newApi: mockNewApi
+}
+
+// 3. 在命名导出中添加
+export {
+  // ...
+  mockNewApi as newApi
+}
+```
+
+## 参考资料
+
+- [Jest 官方文档](https://jestjs.io/)
+- [Testing Library](https://testing-library.com/)
+- [Taro 官方文档](https://taro-docs.jd.com/)
+- [mini-ui-package-standards.md](./mini-ui-package-standards.md) - Mini UI 包开发规范
+- [testing-strategy.md](./testing-strategy.md) - 通用测试策略
+
+## 版本历史
+
+| 版本 | 日期 | 描述 | 作者 |
+|------|------|------|------|
+| 1.0 | 2025-12-26 | 初始版本,基于故事 017.003 实施经验 | Dev Agent |

+ 0 - 6
mini-ui-packages/rencai-personal-info-ui/tests/pages/PersonalInfoPage/PersonalInfoPage.test.tsx

@@ -11,12 +11,6 @@ import { talentPersonalInfoClient } from '../../../src/api'
 import { useRequireAuth } from '@d8d/rencai-auth-ui/hooks'
 import Taro from '@tarojs/taro'
 
-declare const describe: any
-declare const it: any
-declare const expect: any
-declare const jest: any
-declare const beforeEach: any
-
 // 使用 mini-testing-utils 提供的 Taro mock
 // jest.mock('@tarojs/taro') 在 testing/setup.ts 中已配置