# 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 }) => ( {children} ) } ``` ```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: { // 测试文件中的别名映射(仅用于测试文件) '^@/(.*)$': '/src/$1', '^~/(.*)$': '/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: [ '/tests/**/*.spec.{ts,tsx}', '/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// └── 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() 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 = (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 }) => ( {children} ) } 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(, { wrapper }) await waitFor(() => { expect(screen.getByText('expected text')).toBeInTheDocument() }) }) }) ``` ### 3. Mock RPC 响应规范 ```typescript // ✅ 正确: 使用 Mock 响应工具函数 const createMockResponse = (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() 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(, { wrapper }) await waitFor(() => { expect(screen.getByText('data')).toBeInTheDocument() }) }) ``` ### 3. 用户交互测试 ```typescript it('should handle click event', () => { const { fireEvent } = require('@testing-library/react') render() 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(, { 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/ && 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 |