@qlover/create-app 0.6.3 → 0.7.1
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 +29 -0
- package/dist/configs/node-lib/eslint.config.js +3 -3
- package/dist/configs/react-app/eslint.config.js +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/pack-app/eslint.config.js +3 -3
- package/dist/templates/pack-app/package.json +1 -1
- 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/test-guide.md +782 -0
- package/dist/templates/react-app/docs/zh/test-guide.md +782 -0
- package/dist/templates/react-app/package.json +9 -20
- package/dist/templates/react-app/public/locales/en/common.json +1 -1
- package/dist/templates/react-app/public/locales/zh/common.json +1 -1
- 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/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 +3 -3
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
> This document briefly introduces the testing strategies and best practices for the **fe-base** project in a monorepo scenario, using [Vitest](https://vitest.dev/) as the unified testing framework.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why Choose Vitest
|
|
8
|
+
|
|
9
|
+
1. **Perfect Integration with Vite Ecosystem**: Shares Vite configuration, TypeScript & ESM work out of the box.
|
|
10
|
+
2. **Modern Features**: Parallel execution, HMR, built-in coverage statistics.
|
|
11
|
+
3. **Jest Compatible API**: `describe / it / expect` APIs with no learning curve.
|
|
12
|
+
4. **Monorepo Friendly**: Can filter execution by workspace, easy to run package-level tests in parallel in CI.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Test Types
|
|
17
|
+
|
|
18
|
+
- **Unit Tests**: Verify the minimal behavior of functions, classes, or components.
|
|
19
|
+
- **Integration Tests**: Verify collaboration and boundaries between multiple modules.
|
|
20
|
+
- **End-to-End (E2E, introduce Playwright/Cypress as needed)**: Verify complete user workflows.
|
|
21
|
+
|
|
22
|
+
> ⚡️ In most cases, prioritize writing unit tests; only add integration tests when cross-module interactions are complex.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Test File Organization Standards
|
|
27
|
+
|
|
28
|
+
### File Naming and Location
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
packages/
|
|
32
|
+
├── package-name/
|
|
33
|
+
│ ├── __tests__/ # Test files directory
|
|
34
|
+
│ │ ├── Class.test.ts # Class tests
|
|
35
|
+
│ │ ├── utils/ # Utility function tests
|
|
36
|
+
│ │ │ └── helper.test.ts
|
|
37
|
+
│ │ └── integration/ # Integration tests
|
|
38
|
+
│ ├── __mocks__/ # Mock files directory
|
|
39
|
+
│ │ └── index.ts
|
|
40
|
+
│ └── src/ # Source code directory
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Test File Structure
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Standard test file header comment
|
|
47
|
+
/**
|
|
48
|
+
* ClassName test-suite
|
|
49
|
+
*
|
|
50
|
+
* Coverage:
|
|
51
|
+
* 1. constructor – Constructor tests
|
|
52
|
+
* 2. methodName – Method functionality tests
|
|
53
|
+
* 3. edge cases – Edge case tests
|
|
54
|
+
* 4. error handling – Error handling tests
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
58
|
+
import { ClassName } from '../src/ClassName';
|
|
59
|
+
|
|
60
|
+
describe('ClassName', () => {
|
|
61
|
+
// Test data and mock objects
|
|
62
|
+
let instance: ClassName;
|
|
63
|
+
let mockDependency: MockType;
|
|
64
|
+
|
|
65
|
+
// Setup and cleanup
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
// Initialize test environment
|
|
68
|
+
mockDependency = createMockDependency();
|
|
69
|
+
instance = new ClassName(mockDependency);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
// Clean up test environment
|
|
74
|
+
vi.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Constructor tests
|
|
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
|
+
// Method test grouping
|
|
89
|
+
describe('methodName', () => {
|
|
90
|
+
it('should handle normal case', () => {
|
|
91
|
+
// Test normal scenarios
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle edge cases', () => {
|
|
95
|
+
// Test edge cases
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should handle error cases', () => {
|
|
99
|
+
// Test error scenarios
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Integration tests
|
|
104
|
+
describe('integration tests', () => {
|
|
105
|
+
it('should work with dependent modules', () => {
|
|
106
|
+
// Test module collaboration
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Vitest Global Configuration Example
|
|
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
|
+
// Automatically mock certain packages in test environment, pointing to __mocks__ directory
|
|
128
|
+
'@qlover/fe-corekit': resolve(__dirname, 'packages/fe-corekit/__mocks__'),
|
|
129
|
+
'@qlover/logger': resolve(__dirname, 'packages/logger/__mocks__')
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Package-level Scripts
|
|
136
|
+
|
|
137
|
+
```jsonc
|
|
138
|
+
// packages/xxx/package.json (example)
|
|
139
|
+
{
|
|
140
|
+
"scripts": {
|
|
141
|
+
"test": "vitest run",
|
|
142
|
+
"test:watch": "vitest",
|
|
143
|
+
"test:coverage": "vitest run --coverage"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Test Strategy
|
|
151
|
+
|
|
152
|
+
### Test Grouping
|
|
153
|
+
|
|
154
|
+
The entire file is a test file, with test content organized into groups. For example, describe represents a group of tests. Typically, a test file has only one root describe.
|
|
155
|
+
|
|
156
|
+
The content is tested from "small to large." For example, if the source file contains a class, the tests are grouped by the constructor parameters, the constructor itself, and each member method.
|
|
157
|
+
|
|
158
|
+
- From covering various parameter types for each method to the overall flow affected by method calls.
|
|
159
|
+
- As well as comprehensive flow testing and boundary testing.
|
|
160
|
+
|
|
161
|
+
Source file (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
|
+
Test file (TestClass.test.ts):
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
describe('TestClass', () => {
|
|
185
|
+
describe('TestClass.constructor', () => {
|
|
186
|
+
// ...
|
|
187
|
+
});
|
|
188
|
+
describe('TestClass.getName', () => {
|
|
189
|
+
// ...
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('Overall flow or boundary testing', () => {
|
|
193
|
+
it('should keep getName consistent after modifying the name', () => {
|
|
194
|
+
const testClass = new TestClass({ name: 'test' });
|
|
195
|
+
testClass.setName('test2');
|
|
196
|
+
expect(testClass.getName()).toBe('test2');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Test Case Naming Conventions
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
describe('ClassName', () => {
|
|
206
|
+
describe('methodName', () => {
|
|
207
|
+
// Positive test cases
|
|
208
|
+
it('should return expected result when given valid input', () => {});
|
|
209
|
+
it('should handle multiple parameters correctly', () => {});
|
|
210
|
+
|
|
211
|
+
// Boundary test cases
|
|
212
|
+
it('should handle empty input', () => {});
|
|
213
|
+
it('should handle null/undefined input', () => {});
|
|
214
|
+
it('should handle maximum/minimum values', () => {});
|
|
215
|
+
|
|
216
|
+
// Error test cases
|
|
217
|
+
it('should throw error when given invalid input', () => {});
|
|
218
|
+
it('should handle network failure gracefully', () => {});
|
|
219
|
+
|
|
220
|
+
// Behavioral test cases
|
|
221
|
+
it('should call dependency method with correct parameters', () => {});
|
|
222
|
+
it('should not call dependency when condition is false', () => {});
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Function Testing
|
|
228
|
+
|
|
229
|
+
Function tests should cover the following aspects:
|
|
230
|
+
|
|
231
|
+
1. **Parameter Combination Testing**
|
|
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
|
+
// Implementation...
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
describe('testFunction', () => {
|
|
245
|
+
describe('Parameter Combination Testing', () => {
|
|
246
|
+
it('should handle case with all parameters present', () => {
|
|
247
|
+
expect(testFunction({ key1: 'test', key2: 1, key3: true })).toBe(
|
|
248
|
+
'expected result'
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should handle case with key1, key2 only', () => {
|
|
253
|
+
expect(testFunction({ key1: 'test', key2: 1 })).toBe('expected result');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should handle case with key1, key3 only', () => {
|
|
257
|
+
expect(testFunction({ key1: 'test', key3: true })).toBe(
|
|
258
|
+
'expected result'
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should handle case with key2, key3 only', () => {
|
|
263
|
+
expect(testFunction({ key2: 1, key3: true })).toBe('expected result');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle case with key1 only', () => {
|
|
267
|
+
expect(testFunction({ key1: 'test' })).toBe('expected result');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should handle case with key2 only', () => {
|
|
271
|
+
expect(testFunction({ key2: 1 })).toBe('expected result');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should handle case with key3 only', () => {
|
|
275
|
+
expect(testFunction({ key3: true })).toBe('expected result');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should handle empty object case', () => {
|
|
279
|
+
expect(testFunction({})).toBe('expected result');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('Boundary Value Testing', () => {
|
|
284
|
+
it('should handle extreme values', () => {
|
|
285
|
+
expect(
|
|
286
|
+
testFunction({
|
|
287
|
+
key1: '', // Empty string
|
|
288
|
+
key2: Number.MAX_SAFE_INTEGER, // Maximum safe integer
|
|
289
|
+
key3: false
|
|
290
|
+
})
|
|
291
|
+
).toBe('expected result');
|
|
292
|
+
|
|
293
|
+
expect(
|
|
294
|
+
testFunction({
|
|
295
|
+
key1: 'a'.repeat(1000), // Very long string
|
|
296
|
+
key2: Number.MIN_SAFE_INTEGER, // Minimum safe integer
|
|
297
|
+
key3: true
|
|
298
|
+
})
|
|
299
|
+
).toBe('expected result');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should handle special values', () => {
|
|
303
|
+
expect(
|
|
304
|
+
testFunction({
|
|
305
|
+
key1: ' ', // All spaces string
|
|
306
|
+
key2: 0, // Zero value
|
|
307
|
+
key3: false
|
|
308
|
+
})
|
|
309
|
+
).toBe('expected result');
|
|
310
|
+
|
|
311
|
+
expect(
|
|
312
|
+
testFunction({
|
|
313
|
+
key1: null as any, // null value
|
|
314
|
+
key2: NaN, // NaN value
|
|
315
|
+
key3: undefined as any // undefined value
|
|
316
|
+
})
|
|
317
|
+
).toBe('expected result');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should handle invalid values', () => {
|
|
321
|
+
expect(() =>
|
|
322
|
+
testFunction({
|
|
323
|
+
key1: Symbol() as any, // Invalid type
|
|
324
|
+
key2: {} as any, // Invalid type
|
|
325
|
+
key3: 42 as any // Invalid type
|
|
326
|
+
})
|
|
327
|
+
).toThrow();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
This test suite demonstrates:
|
|
334
|
+
|
|
335
|
+
1. **Complete Parameter Combination Coverage**:
|
|
336
|
+
- Tests all possible parameter combinations (2^n combinations, where n is the number of parameters)
|
|
337
|
+
- Includes cases with all parameters present, partial parameters, and empty object
|
|
338
|
+
|
|
339
|
+
2. **Boundary Value Testing**:
|
|
340
|
+
- Tests parameter limit values (maximum, minimum)
|
|
341
|
+
- Tests special values (empty string, null, undefined, NaN)
|
|
342
|
+
- Tests invalid values (type errors)
|
|
343
|
+
|
|
344
|
+
3. **Test Case Organization**:
|
|
345
|
+
- Uses nested describe blocks to clearly organize test scenarios
|
|
346
|
+
- Each test case has a clear description
|
|
347
|
+
- Related test cases are grouped together
|
|
348
|
+
|
|
349
|
+
### Test Data Management
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
describe('DataProcessor', () => {
|
|
353
|
+
// Test data constants
|
|
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
|
+
// Test data factory functions
|
|
367
|
+
const createTestUser = (overrides = {}) => ({
|
|
368
|
+
id: 1,
|
|
369
|
+
name: 'Test User',
|
|
370
|
+
email: 'test@example.com',
|
|
371
|
+
...overrides
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Complex data structures
|
|
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 Strategy
|
|
392
|
+
|
|
393
|
+
### 1. Global Mock Directory
|
|
394
|
+
|
|
395
|
+
Each package can expose a subdirectory with the same name, providing persistent mocks for automatic use by other packages during testing.
|
|
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. File-level Mock
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// At the top of test file
|
|
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. Dynamic Mock
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
describe('ServiceClass', () => {
|
|
449
|
+
it('should handle API failure', async () => {
|
|
450
|
+
// Temporarily mock API call failure
|
|
451
|
+
vi.spyOn(apiClient, 'request').mockRejectedValue(
|
|
452
|
+
new Error('Network error')
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
await expect(service.fetchData()).rejects.toThrow('Network error');
|
|
456
|
+
|
|
457
|
+
// Restore original implementation
|
|
458
|
+
vi.restoreAllMocks();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 4. Mock Class Instances
|
|
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
|
+
## Test Environment Management
|
|
506
|
+
|
|
507
|
+
### Lifecycle Hooks
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
describe('ComponentTest', () => {
|
|
511
|
+
let component: Component;
|
|
512
|
+
let mockDependency: MockDependency;
|
|
513
|
+
|
|
514
|
+
beforeAll(() => {
|
|
515
|
+
// One-time setup before entire test suite
|
|
516
|
+
setupGlobalTestEnvironment();
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
afterAll(() => {
|
|
520
|
+
// One-time cleanup after entire test suite
|
|
521
|
+
cleanupGlobalTestEnvironment();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
beforeEach(() => {
|
|
525
|
+
// Setup before each test case
|
|
526
|
+
vi.useFakeTimers();
|
|
527
|
+
mockDependency = new MockDependency();
|
|
528
|
+
component = new Component(mockDependency);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
afterEach(() => {
|
|
532
|
+
// Cleanup after each test case
|
|
533
|
+
vi.useRealTimers();
|
|
534
|
+
vi.clearAllMocks();
|
|
535
|
+
mockDependency.reset();
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### File System Testing
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
describe('FileProcessor', () => {
|
|
544
|
+
const testDir = './test-files';
|
|
545
|
+
const testFilePath = path.join(testDir, 'test.json');
|
|
546
|
+
|
|
547
|
+
beforeAll(() => {
|
|
548
|
+
// Create test directories and files
|
|
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
|
+
// Clean up test files
|
|
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
|
+
## Boundary Testing and Error Handling
|
|
573
|
+
|
|
574
|
+
### Boundary Value Testing
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
describe('ValidationUtils', () => {
|
|
578
|
+
describe('validateAge', () => {
|
|
579
|
+
it('should handle boundary values', () => {
|
|
580
|
+
// Boundary value testing
|
|
581
|
+
expect(validateAge(0)).toBe(true); // Minimum value
|
|
582
|
+
expect(validateAge(150)).toBe(true); // Maximum value
|
|
583
|
+
expect(validateAge(-1)).toBe(false); // Below minimum
|
|
584
|
+
expect(validateAge(151)).toBe(false); // Above maximum
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should handle edge cases', () => {
|
|
588
|
+
// Edge case testing
|
|
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
|
+
### Async Operation Testing
|
|
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
|
+
### Type Safety Testing
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
describe('TypeSafetyTests', () => {
|
|
632
|
+
it('should maintain type safety', () => {
|
|
633
|
+
const processor = new DataProcessor<User>();
|
|
634
|
+
|
|
635
|
+
// Use expectTypeOf for type checking
|
|
636
|
+
expectTypeOf(processor.process).parameter(0).toEqualTypeOf<User>();
|
|
637
|
+
expectTypeOf(processor.process).returns.toEqualTypeOf<ProcessedUser>();
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## Performance Testing
|
|
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); // Should complete within 1 second
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## Running Tests
|
|
665
|
+
|
|
666
|
+
```bash
|
|
667
|
+
# Run all package tests
|
|
668
|
+
pnpm test
|
|
669
|
+
|
|
670
|
+
# Run tests for specific package only
|
|
671
|
+
pnpm --filter @qlover/fe-corekit test
|
|
672
|
+
|
|
673
|
+
# Watch mode
|
|
674
|
+
pnpm test:watch
|
|
675
|
+
|
|
676
|
+
# Generate coverage report
|
|
677
|
+
pnpm test:coverage
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
In CI, you can leverage GitHub Actions:
|
|
681
|
+
|
|
682
|
+
```yaml
|
|
683
|
+
# .github/workflows/test.yml (truncated)
|
|
684
|
+
- run: pnpm install
|
|
685
|
+
- run: pnpm test:coverage
|
|
686
|
+
- uses: codecov/codecov-action@v3
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## Coverage Targets
|
|
692
|
+
|
|
693
|
+
| Metric | Target |
|
|
694
|
+
| ---------- | ------ |
|
|
695
|
+
| Statements | ≥ 80% |
|
|
696
|
+
| Branches | ≥ 75% |
|
|
697
|
+
| Functions | ≥ 85% |
|
|
698
|
+
| Lines | ≥ 80% |
|
|
699
|
+
|
|
700
|
+
Coverage reports are output to the `coverage/` directory by default, with `index.html` available for local browsing.
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
## Debugging
|
|
705
|
+
|
|
706
|
+
### VS Code Launch Configuration
|
|
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
|
+
> You can use `console.log` / `debugger` in test code to assist with troubleshooting.
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## Frequently Asked Questions (FAQ)
|
|
730
|
+
|
|
731
|
+
### Q1: How to Mock Browser APIs?
|
|
732
|
+
|
|
733
|
+
Use `vi.mock()` or globally override in `setupFiles`, for example:
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
globalThis.requestAnimationFrame = (cb) => setTimeout(cb, 16);
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Q2: What to do when tests are slow?
|
|
740
|
+
|
|
741
|
+
- Use `vi.useFakeTimers()` to accelerate time-related logic.
|
|
742
|
+
- Break down long integration processes into independent unit tests.
|
|
743
|
+
|
|
744
|
+
### Q3: How to test TypeScript types?
|
|
745
|
+
|
|
746
|
+
Use `expectTypeOf`:
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
import { expectTypeOf } from 'vitest';
|
|
750
|
+
|
|
751
|
+
expectTypeOf(MyUtility.doSomething).returns.toEqualTypeOf<string>();
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Q4: How to test private methods?
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
// Access private methods through type assertion
|
|
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: How to handle dependency injection testing?
|
|
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
|
+
```
|