@qlover/create-app 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/react-app/README.en.md +257 -0
- package/dist/templates/react-app/README.md +29 -231
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +13 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +48 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +16 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +92 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.test.tsx +139 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +288 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +102 -0
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +228 -0
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +207 -0
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +181 -0
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +61 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +199 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +192 -0
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +235 -0
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +224 -0
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +257 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +72 -0
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +62 -0
- package/dist/templates/react-app/__tests__/src/main.test.tsx +46 -0
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +88 -0
- package/dist/templates/react-app/config/app.router.ts +155 -0
- package/dist/templates/react-app/config/common.ts +9 -1
- package/dist/templates/react-app/docs/en/bootstrap.md +562 -0
- package/dist/templates/react-app/docs/en/development-guide.md +523 -0
- package/dist/templates/react-app/docs/en/env.md +482 -0
- package/dist/templates/react-app/docs/en/global.md +509 -0
- package/dist/templates/react-app/docs/en/i18n.md +268 -0
- package/dist/templates/react-app/docs/en/index.md +173 -0
- package/dist/templates/react-app/docs/en/ioc.md +424 -0
- package/dist/templates/react-app/docs/en/project-structure.md +434 -0
- package/dist/templates/react-app/docs/en/request.md +425 -0
- package/dist/templates/react-app/docs/en/router.md +404 -0
- package/dist/templates/react-app/docs/en/store.md +321 -0
- package/dist/templates/react-app/docs/en/test-guide.md +782 -0
- package/dist/templates/react-app/docs/en/theme.md +424 -0
- package/dist/templates/react-app/docs/en/typescript-guide.md +473 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +7 -0
- package/dist/templates/react-app/docs/zh/development-guide.md +523 -0
- package/dist/templates/react-app/docs/zh/env.md +24 -25
- package/dist/templates/react-app/docs/zh/global.md +28 -27
- package/dist/templates/react-app/docs/zh/i18n.md +268 -0
- package/dist/templates/react-app/docs/zh/index.md +173 -0
- package/dist/templates/react-app/docs/zh/ioc.md +44 -32
- package/dist/templates/react-app/docs/zh/project-structure.md +434 -0
- package/dist/templates/react-app/docs/zh/request.md +429 -0
- package/dist/templates/react-app/docs/zh/router.md +408 -0
- package/dist/templates/react-app/docs/zh/store.md +321 -0
- package/dist/templates/react-app/docs/zh/test-guide.md +782 -0
- package/dist/templates/react-app/docs/zh/theme.md +424 -0
- package/dist/templates/react-app/docs/zh/typescript-guide.md +473 -0
- package/dist/templates/react-app/package.json +9 -20
- package/dist/templates/react-app/src/base/cases/AppConfig.ts +16 -9
- package/dist/templates/react-app/src/base/cases/PublicAssetsPath.ts +7 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +15 -4
- package/dist/templates/react-app/src/base/services/RouteService.ts +43 -7
- package/dist/templates/react-app/src/core/bootstraps/BootstrapApp.ts +31 -10
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +1 -1
- package/dist/templates/react-app/src/core/globals.ts +1 -3
- package/dist/templates/react-app/src/core/registers/RegisterCommon.ts +5 -3
- package/dist/templates/react-app/src/main.tsx +6 -1
- package/dist/templates/react-app/src/pages/404.tsx +0 -1
- package/dist/templates/react-app/src/pages/500.tsx +1 -1
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/dark.css +3 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +1 -1
- package/dist/templates/react-app/src/styles/css/antd-themes/pink.css +6 -1
- package/dist/templates/react-app/src/styles/css/page.css +1 -1
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +9 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +5 -3
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +4 -6
- package/dist/templates/react-app/tsconfig.json +2 -1
- package/dist/templates/react-app/tsconfig.test.json +13 -0
- package/dist/templates/react-app/vite.config.ts +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# 测试指南
|
|
2
|
+
|
|
3
|
+
> 本文档简要介绍 **fe-base** 项目在 monorepo 场景下的测试策略与最佳实践,使用 [Vitest](https://vitest.dev/) 作为统一测试框架。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 为何选择 Vitest
|
|
8
|
+
|
|
9
|
+
1. **与 Vite 生态完美集成**:共享 Vite 配置,TypeScript & ESM 开箱即用。
|
|
10
|
+
2. **现代特性**:并行执行、HMR、内置覆盖率统计。
|
|
11
|
+
3. **Jest 兼容 API**:`describe / it / expect` 等 API 无学习成本。
|
|
12
|
+
4. **Monorepo 友好**:可按 workspace 过滤执行,易于在 CI 中并行跑包级测试。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 测试类型
|
|
17
|
+
|
|
18
|
+
- **单元测试 (Unit)**:验证函数、类或组件的最小行为。
|
|
19
|
+
- **集成测试 (Integration)**:验证多个模块的协作与边界。
|
|
20
|
+
- **端到端 (E2E,视需要引入 Playwright/Cypress)**:验证完整用户流程。
|
|
21
|
+
|
|
22
|
+
> ⚡️ 在绝大多数情况下,优先编写单元测试;仅在跨模块交互复杂时再补充集成测试。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 测试文件组织规范
|
|
27
|
+
|
|
28
|
+
### 文件命名与位置
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
packages/
|
|
32
|
+
├── package-name/
|
|
33
|
+
│ ├── __tests__/ # 测试文件目录
|
|
34
|
+
│ │ ├── Class.test.ts # 类测试
|
|
35
|
+
│ │ ├── utils/ # 工具函数测试
|
|
36
|
+
│ │ │ └── helper.test.ts
|
|
37
|
+
│ │ └── integration/ # 集成测试
|
|
38
|
+
│ ├── __mocks__/ # Mock 文件目录
|
|
39
|
+
│ │ └── index.ts
|
|
40
|
+
│ └── src/ # 源代码目录
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 测试文件结构
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// 标准测试文件头部注释
|
|
47
|
+
/**
|
|
48
|
+
* ClassName test-suite
|
|
49
|
+
*
|
|
50
|
+
* Coverage:
|
|
51
|
+
* 1. constructor – 构造函数测试
|
|
52
|
+
* 2. methodName – 方法功能测试
|
|
53
|
+
* 3. edge cases – 边界情况测试
|
|
54
|
+
* 4. error handling – 错误处理测试
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
58
|
+
import { ClassName } from '../src/ClassName';
|
|
59
|
+
|
|
60
|
+
describe('ClassName', () => {
|
|
61
|
+
// 测试数据和Mock对象
|
|
62
|
+
let instance: ClassName;
|
|
63
|
+
let mockDependency: MockType;
|
|
64
|
+
|
|
65
|
+
// 设置和清理
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
// 初始化测试环境
|
|
68
|
+
mockDependency = createMockDependency();
|
|
69
|
+
instance = new ClassName(mockDependency);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
// 清理测试环境
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// 构造函数测试
|
|
78
|
+
describe('constructor', () => {
|
|
79
|
+
it('should create instance with valid parameters', () => {
|
|
80
|
+
expect(instance).toBeInstanceOf(ClassName);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error with invalid parameters', () => {
|
|
84
|
+
expect(() => new ClassName(null)).toThrow();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 方法测试分组
|
|
89
|
+
describe('methodName', () => {
|
|
90
|
+
it('should handle normal case', () => {
|
|
91
|
+
// 测试正常情况
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle edge cases', () => {
|
|
95
|
+
// 测试边界情况
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle error cases', () => {
|
|
99
|
+
// 测试错误情况
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 集成测试
|
|
104
|
+
describe('integration tests', () => {
|
|
105
|
+
it('should work with dependent modules', () => {
|
|
106
|
+
// 测试模块间协作
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Vitest 全局配置示例
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// vitest.config.ts
|
|
118
|
+
import { defineConfig } from 'vitest/config';
|
|
119
|
+
import { resolve } from 'path';
|
|
120
|
+
|
|
121
|
+
export default defineConfig({
|
|
122
|
+
test: {
|
|
123
|
+
globals: true,
|
|
124
|
+
environment: 'jsdom',
|
|
125
|
+
setupFiles: ['./test/setup.ts'],
|
|
126
|
+
alias: {
|
|
127
|
+
// 在测试环境下自动 Mock 某些包,指向 __mocks__ 目录
|
|
128
|
+
'@qlover/fe-corekit': resolve(__dirname, 'packages/fe-corekit/__mocks__'),
|
|
129
|
+
'@qlover/logger': resolve(__dirname, 'packages/logger/__mocks__')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 包级脚本
|
|
136
|
+
|
|
137
|
+
```jsonc
|
|
138
|
+
// packages/xxx/package.json (示例)
|
|
139
|
+
{
|
|
140
|
+
"scripts": {
|
|
141
|
+
"test": "vitest run",
|
|
142
|
+
"test:watch": "vitest",
|
|
143
|
+
"test:coverage": "vitest run --coverage"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 测试策略
|
|
151
|
+
|
|
152
|
+
### 测试分组
|
|
153
|
+
|
|
154
|
+
整个文件为一个测试文件, 测试内容以分组为单位,比如describe为一组测试,一般一个测试文件根只有一个describe
|
|
155
|
+
|
|
156
|
+
内容按照从"小到大测试", 比如: 源文件中是 class 那么从构造器传递参数,构造器,每个成员方法为分组进行测试
|
|
157
|
+
|
|
158
|
+
- 小到每个方法传递各种参数类型的覆盖,大到调用方法时影响的整体流程
|
|
159
|
+
- 以及整体的流程测试,边界测试
|
|
160
|
+
|
|
161
|
+
源文件(TestClass.ts):
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
type TestClassOptions = {
|
|
165
|
+
name: string;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
class TestClass {
|
|
169
|
+
constructor(options: TestClassOptions) {}
|
|
170
|
+
|
|
171
|
+
getName(): string {
|
|
172
|
+
return this.options.name;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setName(name: string): void {
|
|
176
|
+
this.options.name = name;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
测试文件(TestClass.test.ts):
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
describe('TestClass', () => {
|
|
185
|
+
describe('TestClass.constructor', () => {
|
|
186
|
+
// ...
|
|
187
|
+
});
|
|
188
|
+
describe('TestClass.getName', () => {
|
|
189
|
+
// ...
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('整体流程或边界测试', () => {
|
|
193
|
+
it('应该在修改name后,getName保持一致', () => {
|
|
194
|
+
const testClass = new TestClass({ name: 'test' });
|
|
195
|
+
testClass.setName('test2');
|
|
196
|
+
expect(testClass.getName()).toBe('test2');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### 测试用例命名规范
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
describe('ClassName', () => {
|
|
206
|
+
describe('methodName', () => {
|
|
207
|
+
// 正面测试用例
|
|
208
|
+
it('should return expected result when given valid input', () => {});
|
|
209
|
+
it('should handle multiple parameters correctly', () => {});
|
|
210
|
+
|
|
211
|
+
// 边界测试用例
|
|
212
|
+
it('should handle empty input', () => {});
|
|
213
|
+
it('should handle null/undefined input', () => {});
|
|
214
|
+
it('should handle maximum/minimum values', () => {});
|
|
215
|
+
|
|
216
|
+
// 错误测试用例
|
|
217
|
+
it('should throw error when given invalid input', () => {});
|
|
218
|
+
it('should handle network failure gracefully', () => {});
|
|
219
|
+
|
|
220
|
+
// 行为测试用例
|
|
221
|
+
it('should call dependency method with correct parameters', () => {});
|
|
222
|
+
it('should not call dependency when condition is false', () => {});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 函数的测试
|
|
228
|
+
|
|
229
|
+
函数测试应该覆盖以下几个方面:
|
|
230
|
+
|
|
231
|
+
1. **参数组合测试**
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
interface TestParams {
|
|
235
|
+
key1?: string;
|
|
236
|
+
key2?: number;
|
|
237
|
+
key3?: boolean;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function testFunction({ key1, key2, key3 }: TestParams): string {
|
|
241
|
+
// 实现...
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
describe('testFunction', () => {
|
|
245
|
+
describe('参数组合测试', () => {
|
|
246
|
+
it('应该处理所有参数都存在的情况', () => {
|
|
247
|
+
expect(testFunction({ key1: 'test', key2: 1, key3: true })).toBe(
|
|
248
|
+
'expected result'
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('应该处理只有 key1, key2 的情况', () => {
|
|
253
|
+
expect(testFunction({ key1: 'test', key2: 1 })).toBe('expected result');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('应该处理只有 key1, key3 的情况', () => {
|
|
257
|
+
expect(testFunction({ key1: 'test', key3: true })).toBe(
|
|
258
|
+
'expected result'
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('应该处理只有 key2, key3 的情况', () => {
|
|
263
|
+
expect(testFunction({ key2: 1, key3: true })).toBe('expected result');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('应该处理只有 key1 的情况', () => {
|
|
267
|
+
expect(testFunction({ key1: 'test' })).toBe('expected result');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('应该处理只有 key2 的情况', () => {
|
|
271
|
+
expect(testFunction({ key2: 1 })).toBe('expected result');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('应该处理只有 key3 的情况', () => {
|
|
275
|
+
expect(testFunction({ key3: true })).toBe('expected result');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('应该处理空对象的情况', () => {
|
|
279
|
+
expect(testFunction({})).toBe('expected result');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('边界值测试', () => {
|
|
284
|
+
it('应该处理极限值情况', () => {
|
|
285
|
+
expect(
|
|
286
|
+
testFunction({
|
|
287
|
+
key1: '', // 空字符串
|
|
288
|
+
key2: Number.MAX_SAFE_INTEGER, // 最大安全整数
|
|
289
|
+
key3: false
|
|
290
|
+
})
|
|
291
|
+
).toBe('expected result');
|
|
292
|
+
|
|
293
|
+
expect(
|
|
294
|
+
testFunction({
|
|
295
|
+
key1: 'a'.repeat(1000), // 超长字符串
|
|
296
|
+
key2: Number.MIN_SAFE_INTEGER, // 最小安全整数
|
|
297
|
+
key3: true
|
|
298
|
+
})
|
|
299
|
+
).toBe('expected result');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('应该处理特殊值情况', () => {
|
|
303
|
+
expect(
|
|
304
|
+
testFunction({
|
|
305
|
+
key1: ' ', // 全空格字符串
|
|
306
|
+
key2: 0, // 零值
|
|
307
|
+
key3: false
|
|
308
|
+
})
|
|
309
|
+
).toBe('expected result');
|
|
310
|
+
|
|
311
|
+
expect(
|
|
312
|
+
testFunction({
|
|
313
|
+
key1: null as any, // null 值
|
|
314
|
+
key2: NaN, // NaN 值
|
|
315
|
+
key3: undefined as any // undefined 值
|
|
316
|
+
})
|
|
317
|
+
).toBe('expected result');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('应该处理无效值情况', () => {
|
|
321
|
+
expect(() =>
|
|
322
|
+
testFunction({
|
|
323
|
+
key1: Symbol() as any, // 无效类型
|
|
324
|
+
key2: {} as any, // 无效类型
|
|
325
|
+
key3: 42 as any // 无效类型
|
|
326
|
+
})
|
|
327
|
+
).toThrow();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
这个测试套件展示了:
|
|
334
|
+
|
|
335
|
+
1. **完整的参数组合覆盖**:
|
|
336
|
+
- 测试所有可能的参数组合(2^n 种组合,n 为参数个数)
|
|
337
|
+
- 包括全部参数存在、部分参数存在和空对象的情况
|
|
338
|
+
|
|
339
|
+
2. **边界值测试**:
|
|
340
|
+
- 测试参数的极限值(最大值、最小值)
|
|
341
|
+
- 测试特殊值(空字符串、null、undefined、NaN)
|
|
342
|
+
- 测试无效值(类型错误)
|
|
343
|
+
|
|
344
|
+
3. **测试用例组织**:
|
|
345
|
+
- 使用嵌套 describe 清晰组织测试场景
|
|
346
|
+
- 每个测试用例都有明确的描述
|
|
347
|
+
- 相关的测试用例组织在一起
|
|
348
|
+
|
|
349
|
+
### 测试数据管理
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
describe('DataProcessor', () => {
|
|
353
|
+
// 测试数据常量
|
|
354
|
+
const VALID_DATA = {
|
|
355
|
+
id: 1,
|
|
356
|
+
name: 'test',
|
|
357
|
+
items: ['a', 'b', 'c']
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const INVALID_DATA = {
|
|
361
|
+
id: null,
|
|
362
|
+
name: '',
|
|
363
|
+
items: []
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// 测试数据工厂函数
|
|
367
|
+
const createTestUser = (overrides = {}) => ({
|
|
368
|
+
id: 1,
|
|
369
|
+
name: 'Test User',
|
|
370
|
+
email: 'test@example.com',
|
|
371
|
+
...overrides
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// 复杂数据结构
|
|
375
|
+
const createComplexTestData = () => ({
|
|
376
|
+
metadata: {
|
|
377
|
+
version: '1.0.0',
|
|
378
|
+
created: Date.now(),
|
|
379
|
+
tags: ['test', 'data']
|
|
380
|
+
},
|
|
381
|
+
users: [
|
|
382
|
+
createTestUser({ id: 1 }),
|
|
383
|
+
createTestUser({ id: 2, name: 'Another User' })
|
|
384
|
+
]
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Mock 策略
|
|
392
|
+
|
|
393
|
+
### 1. 全局 Mock 目录
|
|
394
|
+
|
|
395
|
+
每个包可暴露同名子目录,提供持久 Mock,供其他包在测试时自动使用。
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
// packages/fe-corekit/__mocks__/index.ts
|
|
399
|
+
import { vi } from 'vitest';
|
|
400
|
+
|
|
401
|
+
export const MyUtility = {
|
|
402
|
+
doSomething: vi.fn(() => 'mocked'),
|
|
403
|
+
processData: vi.fn((input: string) => `processed-${input}`)
|
|
404
|
+
};
|
|
405
|
+
export default MyUtility;
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 2. 文件级 Mock
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// 在测试文件顶部
|
|
412
|
+
vi.mock('../src/Util', () => ({
|
|
413
|
+
Util: {
|
|
414
|
+
ensureDir: vi.fn(),
|
|
415
|
+
readFile: vi.fn()
|
|
416
|
+
}
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
vi.mock('js-cookie', () => {
|
|
420
|
+
let store: Record<string, string> = {};
|
|
421
|
+
|
|
422
|
+
const get = vi.fn((key?: string) => {
|
|
423
|
+
if (typeof key === 'string') return store[key];
|
|
424
|
+
return { ...store };
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const set = vi.fn((key: string, value: string) => {
|
|
428
|
+
store[key] = value;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const remove = vi.fn((key: string) => {
|
|
432
|
+
delete store[key];
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const __resetStore = () => {
|
|
436
|
+
store = {};
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
default: { get, set, remove, __resetStore }
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 3. 动态 Mock
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
describe('ServiceClass', () => {
|
|
449
|
+
it('should handle API failure', async () => {
|
|
450
|
+
// 临时 Mock API 调用失败
|
|
451
|
+
vi.spyOn(apiClient, 'request').mockRejectedValue(
|
|
452
|
+
new Error('Network error')
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
await expect(service.fetchData()).rejects.toThrow('Network error');
|
|
456
|
+
|
|
457
|
+
// 恢复原始实现
|
|
458
|
+
vi.restoreAllMocks();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 4. Mock 类实例
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
class MockStorage<Key = string> implements SyncStorageInterface<Key> {
|
|
467
|
+
public data = new Map<string, string>();
|
|
468
|
+
public calls: {
|
|
469
|
+
setItem: Array<{ key: Key; value: unknown; options?: unknown }>;
|
|
470
|
+
getItem: Array<{ key: Key; defaultValue?: unknown; options?: unknown }>;
|
|
471
|
+
removeItem: Array<{ key: Key; options?: unknown }>;
|
|
472
|
+
clear: number;
|
|
473
|
+
} = {
|
|
474
|
+
setItem: [],
|
|
475
|
+
getItem: [],
|
|
476
|
+
removeItem: [],
|
|
477
|
+
clear: 0
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
setItem<T>(key: Key, value: T, options?: unknown): void {
|
|
481
|
+
this.calls.setItem.push({ key, value, options });
|
|
482
|
+
this.data.set(String(key), String(value));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
getItem<T>(key: Key, defaultValue?: T, options?: unknown): T | null {
|
|
486
|
+
this.calls.getItem.push({ key, defaultValue, options });
|
|
487
|
+
const value = this.data.get(String(key));
|
|
488
|
+
return (value ?? defaultValue ?? null) as T | null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
reset(): void {
|
|
492
|
+
this.data.clear();
|
|
493
|
+
this.calls = {
|
|
494
|
+
setItem: [],
|
|
495
|
+
getItem: [],
|
|
496
|
+
removeItem: [],
|
|
497
|
+
clear: 0
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## 测试环境管理
|
|
506
|
+
|
|
507
|
+
### 生命周期钩子
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
describe('ComponentTest', () => {
|
|
511
|
+
let component: Component;
|
|
512
|
+
let mockDependency: MockDependency;
|
|
513
|
+
|
|
514
|
+
beforeAll(() => {
|
|
515
|
+
// 整个测试套件运行前的一次性设置
|
|
516
|
+
setupGlobalTestEnvironment();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
afterAll(() => {
|
|
520
|
+
// 整个测试套件运行后的一次性清理
|
|
521
|
+
cleanupGlobalTestEnvironment();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
beforeEach(() => {
|
|
525
|
+
// 每个测试用例前的设置
|
|
526
|
+
vi.useFakeTimers();
|
|
527
|
+
mockDependency = new MockDependency();
|
|
528
|
+
component = new Component(mockDependency);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
afterEach(() => {
|
|
532
|
+
// 每个测试用例后的清理
|
|
533
|
+
vi.useRealTimers();
|
|
534
|
+
vi.clearAllMocks();
|
|
535
|
+
mockDependency.reset();
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 文件系统测试
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
describe('FileProcessor', () => {
|
|
544
|
+
const testDir = './test-files';
|
|
545
|
+
const testFilePath = path.join(testDir, 'test.json');
|
|
546
|
+
|
|
547
|
+
beforeAll(() => {
|
|
548
|
+
// 创建测试目录和文件
|
|
549
|
+
if (!fs.existsSync(testDir)) {
|
|
550
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
551
|
+
}
|
|
552
|
+
fs.writeFileSync(testFilePath, JSON.stringify({ test: 'data' }));
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
afterAll(() => {
|
|
556
|
+
// 清理测试文件
|
|
557
|
+
if (fs.existsSync(testDir)) {
|
|
558
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it('should process file correctly', () => {
|
|
563
|
+
const processor = new FileProcessor();
|
|
564
|
+
const result = processor.processFile(testFilePath);
|
|
565
|
+
expect(result).toEqual({ test: 'data' });
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
---
|
|
571
|
+
|
|
572
|
+
## 边界测试与错误处理
|
|
573
|
+
|
|
574
|
+
### 边界值测试
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
describe('ValidationUtils', () => {
|
|
578
|
+
describe('validateAge', () => {
|
|
579
|
+
it('should handle boundary values', () => {
|
|
580
|
+
// 边界值测试
|
|
581
|
+
expect(validateAge(0)).toBe(true); // 最小值
|
|
582
|
+
expect(validateAge(150)).toBe(true); // 最大值
|
|
583
|
+
expect(validateAge(-1)).toBe(false); // 小于最小值
|
|
584
|
+
expect(validateAge(151)).toBe(false); // 大于最大值
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should handle edge cases', () => {
|
|
588
|
+
// 边界情况测试
|
|
589
|
+
expect(validateAge(null)).toBe(false);
|
|
590
|
+
expect(validateAge(undefined)).toBe(false);
|
|
591
|
+
expect(validateAge(NaN)).toBe(false);
|
|
592
|
+
expect(validateAge(Infinity)).toBe(false);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### 异步操作测试
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
describe('AsyncService', () => {
|
|
602
|
+
it('should handle successful async operation', async () => {
|
|
603
|
+
const service = new AsyncService();
|
|
604
|
+
const result = await service.fetchData();
|
|
605
|
+
expect(result).toBeDefined();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('should handle async operation failure', async () => {
|
|
609
|
+
const service = new AsyncService();
|
|
610
|
+
vi.spyOn(service, 'apiCall').mockRejectedValue(new Error('API Error'));
|
|
611
|
+
|
|
612
|
+
await expect(service.fetchData()).rejects.toThrow('API Error');
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
it('should handle timeout', async () => {
|
|
616
|
+
vi.useFakeTimers();
|
|
617
|
+
const service = new AsyncService();
|
|
618
|
+
|
|
619
|
+
const promise = service.fetchDataWithTimeout(1000);
|
|
620
|
+
vi.advanceTimersByTime(1001);
|
|
621
|
+
|
|
622
|
+
await expect(promise).rejects.toThrow('Timeout');
|
|
623
|
+
vi.useRealTimers();
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### 类型安全测试
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
describe('TypeSafetyTests', () => {
|
|
632
|
+
it('should maintain type safety', () => {
|
|
633
|
+
const processor = new DataProcessor<User>();
|
|
634
|
+
|
|
635
|
+
// 使用 expectTypeOf 进行类型检查
|
|
636
|
+
expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
|
|
637
|
+
expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## 性能测试
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
describe('PerformanceTests', () => {
|
|
648
|
+
it('should complete operation within time limit', async () => {
|
|
649
|
+
const startTime = Date.now();
|
|
650
|
+
const processor = new DataProcessor();
|
|
651
|
+
|
|
652
|
+
await processor.processLargeDataset(largeDataset);
|
|
653
|
+
|
|
654
|
+
const endTime = Date.now();
|
|
655
|
+
const duration = endTime - startTime;
|
|
656
|
+
|
|
657
|
+
expect(duration).toBeLessThan(1000); // 应在1秒内完成
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## 运行测试
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
# 运行所有包测试
|
|
668
|
+
pnpm test
|
|
669
|
+
|
|
670
|
+
# 仅运行指定包
|
|
671
|
+
pnpm --filter @qlover/fe-corekit test
|
|
672
|
+
|
|
673
|
+
# 监听模式
|
|
674
|
+
pnpm test:watch
|
|
675
|
+
|
|
676
|
+
# 生成覆盖率报告
|
|
677
|
+
pnpm test:coverage
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
在 CI 中,可借助 GitHub Actions:
|
|
681
|
+
|
|
682
|
+
```yaml
|
|
683
|
+
# .github/workflows/test.yml (截断)
|
|
684
|
+
- run: pnpm install
|
|
685
|
+
- run: pnpm test:coverage
|
|
686
|
+
- uses: codecov/codecov-action@v3
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## 覆盖率目标
|
|
692
|
+
|
|
693
|
+
| 指标 | 目标 |
|
|
694
|
+
| ---- | ----- |
|
|
695
|
+
| 语句 | ≥ 80% |
|
|
696
|
+
| 分支 | ≥ 75% |
|
|
697
|
+
| 函数 | ≥ 85% |
|
|
698
|
+
| 行数 | ≥ 80% |
|
|
699
|
+
|
|
700
|
+
覆盖率报告默认输出至 `coverage/` 目录,`index.html` 可本地浏览。
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## 调试
|
|
705
|
+
|
|
706
|
+
### VS Code Launch 配置
|
|
707
|
+
|
|
708
|
+
```jsonc
|
|
709
|
+
{
|
|
710
|
+
"version": "0.2.0",
|
|
711
|
+
"configurations": [
|
|
712
|
+
{
|
|
713
|
+
"name": "Debug Vitest",
|
|
714
|
+
"type": "node",
|
|
715
|
+
"request": "launch",
|
|
716
|
+
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
|
|
717
|
+
"args": ["run", "--reporter=verbose"],
|
|
718
|
+
"console": "integratedTerminal",
|
|
719
|
+
"internalConsoleOptions": "neverOpen"
|
|
720
|
+
}
|
|
721
|
+
]
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
> 在测试代码中可使用 `console.log` / `debugger` 辅助排查。
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## 常见问题解答 (FAQ)
|
|
730
|
+
|
|
731
|
+
### Q1 : 如何 Mock 浏览器 API?
|
|
732
|
+
|
|
733
|
+
使用 `vi.mock()` 或在 `setupFiles` 中全局覆写,例如:
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 16);
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Q2 : 测试过慢怎么办?
|
|
740
|
+
|
|
741
|
+
- 使用 `vi.useFakeTimers()` 加速时间相关逻辑。
|
|
742
|
+
- 拆分长时间集成流程为独立单元测试。
|
|
743
|
+
|
|
744
|
+
### Q3 : 如何测试 TypeScript 类型?
|
|
745
|
+
|
|
746
|
+
利用 `expectTypeOf`:
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
import { expectTypeOf } from 'vitest';
|
|
750
|
+
|
|
751
|
+
expectTypeOf(MyUtility.doSomething).returns.toEqualTypeOf<string>();
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Q4 : 如何测试私有方法?
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
// 通过类型断言访问私有方法
|
|
758
|
+
it('should test private method', () => {
|
|
759
|
+
const instance = new MyClass();
|
|
760
|
+
const result = (instance as any).privateMethod();
|
|
761
|
+
expect(result).toBe('expected');
|
|
762
|
+
});
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
### Q5 : 如何处理依赖注入的测试?
|
|
766
|
+
|
|
767
|
+
```typescript
|
|
768
|
+
describe('ServiceWithDependencies', () => {
|
|
769
|
+
let mockRepository: MockRepository;
|
|
770
|
+
let service: UserService;
|
|
771
|
+
|
|
772
|
+
beforeEach(() => {
|
|
773
|
+
mockRepository = new MockRepository();
|
|
774
|
+
service = new UserService(mockRepository);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('should use injected dependency', () => {
|
|
778
|
+
service.getUser(1);
|
|
779
|
+
expect(mockRepository.findById).toHaveBeenCalledWith(1);
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
```
|