specweave 0.26.4 → 0.26.5
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/CLAUDE.md +120 -0
- package/dist/src/core/increment/increment-reopener.d.ts.map +1 -1
- package/dist/src/core/increment/increment-reopener.js +13 -14
- package/dist/src/core/increment/increment-reopener.js.map +1 -1
- package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
- package/dist/src/core/increment/metadata-manager.js +16 -0
- package/dist/src/core/increment/metadata-manager.js.map +1 -1
- package/dist/src/core/increment/status-change-sync-trigger.d.ts +85 -0
- package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -0
- package/dist/src/core/increment/status-change-sync-trigger.js +137 -0
- package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -0
- package/dist/src/core/increment/sync-circuit-breaker.d.ts +64 -0
- package/dist/src/core/increment/sync-circuit-breaker.d.ts.map +1 -0
- package/dist/src/core/increment/sync-circuit-breaker.js +95 -0
- package/dist/src/core/increment/sync-circuit-breaker.js.map +1 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts +12 -0
- package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
- package/dist/src/core/living-docs/living-docs-sync.js +114 -18
- package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
- package/package.json +2 -2
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +16 -0
- package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
- package/plugins/specweave/skills/brownfield-analyzer/SKILL.md +267 -868
- package/plugins/specweave/skills/increment-planner/SKILL.md +337 -1253
- package/plugins/specweave/skills/role-orchestrator/SKILL.md +293 -969
- package/plugins/specweave-docs/skills/technical-writing/SKILL.md +333 -839
- package/plugins/specweave-testing/skills/tdd-expert/SKILL.md +269 -749
- package/plugins/specweave-testing/skills/unit-testing-expert/SKILL.md +318 -810
|
@@ -5,1007 +5,515 @@ description: Comprehensive unit testing expertise covering Vitest, Jest, test-dr
|
|
|
5
5
|
|
|
6
6
|
# Unit Testing Expert
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**Self-contained unit testing expertise for Vitest/Jest in ANY user project.**
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
**Modern Testing Framework** (Vite-native, Jest-compatible)
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
|
-
import { UserService } from './UserService';
|
|
16
|
-
|
|
17
|
-
describe('UserService', () => {
|
|
18
|
-
let userService: UserService;
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
userService = new UserService();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
afterEach(() => {
|
|
25
|
-
vi.clearAllMocks();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should create a new user', () => {
|
|
29
|
-
const user = userService.create({ name: 'John', email: 'john@example.com' });
|
|
30
|
-
|
|
31
|
-
expect(user).toMatchObject({
|
|
32
|
-
id: expect.any(String),
|
|
33
|
-
name: 'John',
|
|
34
|
-
email: 'john@example.com',
|
|
35
|
-
createdAt: expect.any(Date),
|
|
36
|
-
});
|
|
37
|
-
});
|
|
10
|
+
---
|
|
38
11
|
|
|
39
|
-
|
|
40
|
-
expect(() => {
|
|
41
|
-
userService.create({ name: 'John', email: 'invalid' });
|
|
42
|
-
}).toThrow('Invalid email format');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
```
|
|
12
|
+
## Test-Driven Development (TDD)
|
|
46
13
|
|
|
47
|
-
|
|
48
|
-
**Red-Green-Refactor Cycle**
|
|
14
|
+
**Red-Green-Refactor Cycle**:
|
|
49
15
|
|
|
50
16
|
```typescript
|
|
51
|
-
// RED: Write failing test
|
|
17
|
+
// 1. RED: Write failing test
|
|
52
18
|
describe('Calculator', () => {
|
|
53
19
|
it('should add two numbers', () => {
|
|
54
|
-
const
|
|
55
|
-
expect(
|
|
20
|
+
const calc = new Calculator();
|
|
21
|
+
expect(calc.add(2, 3)).toBe(5);
|
|
56
22
|
});
|
|
57
23
|
});
|
|
58
24
|
|
|
59
|
-
// GREEN:
|
|
25
|
+
// 2. GREEN: Minimal implementation
|
|
60
26
|
class Calculator {
|
|
61
27
|
add(a: number, b: number): number {
|
|
62
28
|
return a + b;
|
|
63
29
|
}
|
|
64
30
|
}
|
|
65
31
|
|
|
66
|
-
// REFACTOR: Improve
|
|
32
|
+
// 3. REFACTOR: Improve code
|
|
67
33
|
class Calculator {
|
|
68
34
|
add(...numbers: number[]): number {
|
|
69
|
-
return numbers.reduce((sum,
|
|
35
|
+
return numbers.reduce((sum, n) => sum + n, 0);
|
|
70
36
|
}
|
|
71
37
|
}
|
|
72
|
-
|
|
73
|
-
// Verify tests still pass
|
|
74
|
-
it('should add multiple numbers', () => {
|
|
75
|
-
const calculator = new Calculator();
|
|
76
|
-
expect(calculator.add(1, 2, 3, 4)).toBe(10);
|
|
77
|
-
});
|
|
78
38
|
```
|
|
79
39
|
|
|
80
40
|
**TDD Benefits**:
|
|
81
|
-
-
|
|
82
|
-
- Prevents over-engineering
|
|
41
|
+
- Better design (testable code)
|
|
83
42
|
- Living documentation
|
|
84
|
-
- Confident refactoring
|
|
85
43
|
- Faster debugging
|
|
44
|
+
- Higher confidence
|
|
86
45
|
|
|
87
|
-
|
|
88
|
-
**Structure for Clear Tests**
|
|
46
|
+
---
|
|
89
47
|
|
|
90
|
-
|
|
91
|
-
describe('OrderService', () => {
|
|
92
|
-
it('should calculate total with discount', () => {
|
|
93
|
-
// ARRANGE: Set up test data and dependencies
|
|
94
|
-
const orderService = new OrderService();
|
|
95
|
-
const order = {
|
|
96
|
-
items: [
|
|
97
|
-
{ price: 100, quantity: 2 },
|
|
98
|
-
{ price: 50, quantity: 1 },
|
|
99
|
-
],
|
|
100
|
-
discountCode: 'SAVE20',
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// ACT: Execute the behavior under test
|
|
104
|
-
const total = orderService.calculateTotal(order);
|
|
105
|
-
|
|
106
|
-
// ASSERT: Verify the result
|
|
107
|
-
expect(total).toBe(200); // (100*2 + 50*1) * 0.8 = 200
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
```
|
|
48
|
+
## Vitest/Jest Fundamentals
|
|
111
49
|
|
|
112
|
-
|
|
50
|
+
### Basic Test Structure
|
|
113
51
|
|
|
114
52
|
```typescript
|
|
115
|
-
describe
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
53
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
54
|
+
import { UserService } from './UserService';
|
|
55
|
+
|
|
56
|
+
describe('UserService', () => {
|
|
57
|
+
let service: UserService;
|
|
120
58
|
|
|
121
|
-
|
|
122
|
-
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
service = new UserService();
|
|
61
|
+
});
|
|
123
62
|
|
|
124
|
-
|
|
125
|
-
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
vi.clearAllMocks();
|
|
126
65
|
});
|
|
127
|
-
});
|
|
128
|
-
```
|
|
129
66
|
|
|
130
|
-
|
|
131
|
-
|
|
67
|
+
it('should create user', () => {
|
|
68
|
+
const user = service.create({ name: 'John', email: 'john@test.com' });
|
|
132
69
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
describe('EmailService', () => {
|
|
138
|
-
it('should send welcome email on user registration', async () => {
|
|
139
|
-
// Mock external email API
|
|
140
|
-
const mockSendEmail = vi.fn().mockResolvedValue({ success: true });
|
|
141
|
-
const emailService = new EmailService({ sendEmail: mockSendEmail });
|
|
142
|
-
|
|
143
|
-
await emailService.sendWelcomeEmail('user@example.com');
|
|
144
|
-
|
|
145
|
-
// Verify mock was called correctly
|
|
146
|
-
expect(mockSendEmail).toHaveBeenCalledTimes(1);
|
|
147
|
-
expect(mockSendEmail).toHaveBeenCalledWith({
|
|
148
|
-
to: 'user@example.com',
|
|
149
|
-
subject: 'Welcome!',
|
|
150
|
-
body: expect.stringContaining('Welcome to our platform'),
|
|
70
|
+
expect(user).toMatchObject({
|
|
71
|
+
id: expect.any(String),
|
|
72
|
+
name: 'John',
|
|
73
|
+
email: 'john@test.com'
|
|
151
74
|
});
|
|
152
75
|
});
|
|
76
|
+
|
|
77
|
+
it('should throw for invalid email', () => {
|
|
78
|
+
expect(() => {
|
|
79
|
+
service.create({ name: 'John', email: 'invalid' });
|
|
80
|
+
}).toThrow('Invalid email');
|
|
81
|
+
});
|
|
153
82
|
});
|
|
154
83
|
```
|
|
155
84
|
|
|
156
|
-
|
|
157
|
-
```typescript
|
|
158
|
-
describe('Logger', () => {
|
|
159
|
-
it('should log errors to console', () => {
|
|
160
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
161
|
-
|
|
162
|
-
const logger = new Logger();
|
|
163
|
-
logger.error('Something went wrong');
|
|
85
|
+
### Async Testing
|
|
164
86
|
|
|
165
|
-
|
|
87
|
+
```typescript
|
|
88
|
+
it('should fetch user from API', async () => {
|
|
89
|
+
const user = await api.fetchUser('user-123');
|
|
166
90
|
|
|
167
|
-
|
|
91
|
+
expect(user).toEqual({
|
|
92
|
+
id: 'user-123',
|
|
93
|
+
name: 'John Doe'
|
|
168
94
|
});
|
|
169
95
|
});
|
|
170
|
-
```
|
|
171
96
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
it('should fetch user by id', async () => {
|
|
176
|
-
// Stub database query
|
|
177
|
-
const dbStub = {
|
|
178
|
-
query: vi.fn().mockResolvedValue({
|
|
179
|
-
rows: [{ id: 1, name: 'John' }],
|
|
180
|
-
}),
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const repo = new UserRepository(dbStub);
|
|
184
|
-
const user = await repo.findById(1);
|
|
185
|
-
|
|
186
|
-
expect(user).toEqual({ id: 1, name: 'John' });
|
|
187
|
-
});
|
|
97
|
+
// Testing async errors
|
|
98
|
+
it('should handle API errors', async () => {
|
|
99
|
+
await expect(api.fetchUser('invalid')).rejects.toThrow('User not found');
|
|
188
100
|
});
|
|
189
101
|
```
|
|
190
102
|
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
// Fake in-memory database
|
|
194
|
-
class FakeDatabase {
|
|
195
|
-
private data: Map<string, any> = new Map();
|
|
103
|
+
---
|
|
196
104
|
|
|
197
|
-
|
|
198
|
-
this.data.set(key, value);
|
|
199
|
-
}
|
|
105
|
+
## Mocking Strategies
|
|
200
106
|
|
|
201
|
-
|
|
202
|
-
return this.data.get(key);
|
|
203
|
-
}
|
|
107
|
+
### 1. Mock Functions
|
|
204
108
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
describe('CacheService', () => {
|
|
211
|
-
it('should store and retrieve values', async () => {
|
|
212
|
-
const fakeDb = new FakeDatabase();
|
|
213
|
-
const cache = new CacheService(fakeDb);
|
|
109
|
+
```typescript
|
|
110
|
+
// Mock a function
|
|
111
|
+
const mockFn = vi.fn();
|
|
112
|
+
mockFn.mockReturnValue(42);
|
|
113
|
+
expect(mockFn()).toBe(42);
|
|
214
114
|
|
|
215
|
-
|
|
216
|
-
|
|
115
|
+
// Mock with implementation
|
|
116
|
+
const mockAdd = vi.fn((a, b) => a + b);
|
|
117
|
+
expect(mockAdd(2, 3)).toBe(5);
|
|
217
118
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
119
|
+
// Verify calls
|
|
120
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(mockFn).toHaveBeenCalledWith(expected);
|
|
221
122
|
```
|
|
222
123
|
|
|
223
|
-
###
|
|
224
|
-
**Mock Entire Modules**
|
|
124
|
+
### 2. Mock Modules
|
|
225
125
|
|
|
226
126
|
```typescript
|
|
227
|
-
// Mock
|
|
127
|
+
// Mock entire module
|
|
228
128
|
vi.mock('./database', () => ({
|
|
229
|
-
|
|
230
|
-
connect: vi.fn().mockResolvedValue(true),
|
|
231
|
-
query: vi.fn().mockResolvedValue({ rows: [] }),
|
|
232
|
-
disconnect: vi.fn(),
|
|
233
|
-
})),
|
|
129
|
+
query: vi.fn().mockResolvedValue([{ id: 1, name: 'Test' }])
|
|
234
130
|
}));
|
|
235
131
|
|
|
236
|
-
import {
|
|
237
|
-
import { UserService } from './UserService';
|
|
132
|
+
import { query } from './database';
|
|
238
133
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
await userService.init();
|
|
243
|
-
|
|
244
|
-
expect(Database).toHaveBeenCalledTimes(1);
|
|
245
|
-
});
|
|
134
|
+
it('should fetch users from database', async () => {
|
|
135
|
+
const users = await query('SELECT * FROM users');
|
|
136
|
+
expect(users).toHaveLength(1);
|
|
246
137
|
});
|
|
247
138
|
```
|
|
248
139
|
|
|
249
|
-
|
|
140
|
+
### 3. Spies
|
|
250
141
|
|
|
251
142
|
```typescript
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
...actual,
|
|
256
|
-
// Mock only specific functions
|
|
257
|
-
fetchData: vi.fn().mockResolvedValue({ data: 'mocked' }),
|
|
258
|
-
};
|
|
259
|
-
});
|
|
260
|
-
```
|
|
143
|
+
// Spy on existing method
|
|
144
|
+
const spy = vi.spyOn(console, 'log');
|
|
261
145
|
|
|
262
|
-
|
|
146
|
+
myFunction();
|
|
263
147
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// Hoist mocks to top (before imports)
|
|
268
|
-
vi.hoisted(() => {
|
|
269
|
-
vi.mock('./config', () => ({
|
|
270
|
-
API_URL: 'https://test-api.example.com',
|
|
271
|
-
}));
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
import { API_URL } from './config';
|
|
148
|
+
expect(spy).toHaveBeenCalledWith('Expected message');
|
|
149
|
+
spy.mockRestore();
|
|
275
150
|
```
|
|
276
151
|
|
|
277
|
-
###
|
|
278
|
-
**Handle Promises, Timers, and Callbacks**
|
|
152
|
+
### 4. Mock Dependencies
|
|
279
153
|
|
|
280
|
-
#### Testing Promises
|
|
281
154
|
```typescript
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const service = new AsyncService();
|
|
285
|
-
|
|
286
|
-
const result = await service.fetchData();
|
|
287
|
-
|
|
288
|
-
expect(result).toEqual({ id: 1, name: 'Test' });
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('should reject with error', async () => {
|
|
292
|
-
const service = new AsyncService();
|
|
155
|
+
class UserService {
|
|
156
|
+
constructor(private db: Database) {}
|
|
293
157
|
|
|
294
|
-
|
|
295
|
-
|
|
158
|
+
async getUser(id: string) {
|
|
159
|
+
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
296
162
|
|
|
297
|
-
|
|
298
|
-
|
|
163
|
+
// Test with mock
|
|
164
|
+
const mockDb = {
|
|
165
|
+
query: vi.fn().mockResolvedValue({ id: '123', name: 'John' })
|
|
166
|
+
};
|
|
299
167
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
service.fetchPosts(1),
|
|
303
|
-
]);
|
|
168
|
+
const service = new UserService(mockDb);
|
|
169
|
+
const user = await service.getUser('123');
|
|
304
170
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
171
|
+
expect(mockDb.query).toHaveBeenCalledWith(
|
|
172
|
+
'SELECT * FROM users WHERE id = ?',
|
|
173
|
+
['123']
|
|
174
|
+
);
|
|
309
175
|
```
|
|
310
176
|
|
|
311
|
-
|
|
312
|
-
```typescript
|
|
313
|
-
describe('DebounceService', () => {
|
|
314
|
-
beforeEach(() => {
|
|
315
|
-
vi.useFakeTimers();
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
afterEach(() => {
|
|
319
|
-
vi.restoreAllTimers();
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('should debounce function calls', () => {
|
|
323
|
-
const callback = vi.fn();
|
|
324
|
-
const debounced = debounce(callback, 1000);
|
|
325
|
-
|
|
326
|
-
debounced();
|
|
327
|
-
debounced();
|
|
328
|
-
debounced();
|
|
329
|
-
|
|
330
|
-
expect(callback).not.toHaveBeenCalled();
|
|
331
|
-
|
|
332
|
-
// Fast-forward time
|
|
333
|
-
vi.advanceTimersByTime(1000);
|
|
177
|
+
---
|
|
334
178
|
|
|
335
|
-
|
|
336
|
-
});
|
|
179
|
+
## Test Patterns
|
|
337
180
|
|
|
338
|
-
|
|
339
|
-
const callback = vi.fn();
|
|
340
|
-
const debounced = debounce(callback, 1000);
|
|
181
|
+
### AAA Pattern (Arrange-Act-Assert)
|
|
341
182
|
|
|
342
|
-
|
|
343
|
-
|
|
183
|
+
```typescript
|
|
184
|
+
it('should calculate total price', () => {
|
|
185
|
+
// Arrange
|
|
186
|
+
const cart = new ShoppingCart();
|
|
187
|
+
cart.addItem({ price: 10, quantity: 2 });
|
|
188
|
+
cart.addItem({ price: 5, quantity: 3 });
|
|
344
189
|
|
|
345
|
-
|
|
190
|
+
// Act
|
|
191
|
+
const total = cart.getTotal();
|
|
346
192
|
|
|
347
|
-
|
|
348
|
-
|
|
193
|
+
// Assert
|
|
194
|
+
expect(total).toBe(35);
|
|
349
195
|
});
|
|
350
196
|
```
|
|
351
197
|
|
|
352
|
-
|
|
353
|
-
```typescript
|
|
354
|
-
describe('EventEmitter', () => {
|
|
355
|
-
it('should execute callback on event', (done) => {
|
|
356
|
-
const emitter = new EventEmitter();
|
|
357
|
-
|
|
358
|
-
emitter.on('data', (data) => {
|
|
359
|
-
expect(data).toBe('test');
|
|
360
|
-
done(); // Signal async completion
|
|
361
|
-
});
|
|
198
|
+
### Given-When-Then (BDD)
|
|
362
199
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const promise = new Promise((resolve) => {
|
|
371
|
-
emitter.on('data', resolve);
|
|
372
|
-
});
|
|
200
|
+
```typescript
|
|
201
|
+
describe('Shopping Cart', () => {
|
|
202
|
+
it('should apply discount when total exceeds $100', () => {
|
|
203
|
+
// Given: A cart with items totaling $120
|
|
204
|
+
const cart = new ShoppingCart();
|
|
205
|
+
cart.addItem({ price: 120, quantity: 1 });
|
|
373
206
|
|
|
374
|
-
|
|
207
|
+
// When: Getting the total
|
|
208
|
+
const total = cart.getTotal();
|
|
375
209
|
|
|
376
|
-
|
|
210
|
+
// Then: 10% discount applied
|
|
211
|
+
expect(total).toBe(108); // $120 - $12 (10%)
|
|
377
212
|
});
|
|
378
213
|
});
|
|
379
214
|
```
|
|
380
215
|
|
|
381
|
-
###
|
|
382
|
-
**Test Multiple Cases Efficiently**
|
|
216
|
+
### Parametric Testing
|
|
383
217
|
|
|
384
218
|
```typescript
|
|
385
219
|
describe.each([
|
|
386
|
-
{ input: 2, expected: 4 },
|
|
387
|
-
{ input: 3, expected: 9 },
|
|
388
|
-
{ input: 4, expected: 16 },
|
|
389
|
-
{ input: 5, expected: 25 },
|
|
390
|
-
])('square($input)', ({ input, expected }) => {
|
|
391
|
-
it(`should return ${expected}`, () => {
|
|
392
|
-
expect(square(input)).toBe(expected);
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
// Alternative syntax
|
|
397
|
-
it.each([
|
|
398
|
-
[1, 2, 3],
|
|
399
220
|
[2, 3, 5],
|
|
400
|
-
[
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
{ email: 'user@example.com', valid: true },
|
|
408
|
-
{ email: 'invalid', valid: false },
|
|
409
|
-
{ email: 'missing@', valid: false },
|
|
410
|
-
{ email: '@domain.com', valid: false },
|
|
411
|
-
{ email: '', valid: false },
|
|
412
|
-
])('validateEmail($email)', ({ email, valid }) => {
|
|
413
|
-
it(`should return ${valid}`, () => {
|
|
414
|
-
expect(validateEmail(email)).toBe(valid);
|
|
221
|
+
[10, 5, 15],
|
|
222
|
+
[-1, 1, 0],
|
|
223
|
+
[0, 0, 0]
|
|
224
|
+
])('Calculator.add(%i, %i)', (a, b, expected) => {
|
|
225
|
+
it(`should return ${expected}`, () => {
|
|
226
|
+
const calc = new Calculator();
|
|
227
|
+
expect(calc.add(a, b)).toBe(expected);
|
|
415
228
|
});
|
|
416
229
|
});
|
|
417
230
|
```
|
|
418
231
|
|
|
419
|
-
|
|
420
|
-
**Capture and Compare Complex Outputs**
|
|
421
|
-
|
|
422
|
-
```typescript
|
|
423
|
-
describe('ComponentRenderer', () => {
|
|
424
|
-
it('should render user profile correctly', () => {
|
|
425
|
-
const user = { id: 1, name: 'John', email: 'john@example.com' };
|
|
426
|
-
const rendered = renderUserProfile(user);
|
|
427
|
-
|
|
428
|
-
expect(rendered).toMatchSnapshot();
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('should render empty state', () => {
|
|
432
|
-
const rendered = renderUserProfile(null);
|
|
232
|
+
---
|
|
433
233
|
|
|
434
|
-
|
|
435
|
-
});
|
|
234
|
+
## Test Doubles
|
|
436
235
|
|
|
437
|
-
|
|
438
|
-
it('should format date', () => {
|
|
439
|
-
const formatted = formatDate(new Date('2025-01-15'));
|
|
236
|
+
### Mocks vs Stubs vs Spies vs Fakes
|
|
440
237
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
238
|
+
**Mock**: Verifies behavior (calls, arguments)
|
|
239
|
+
```typescript
|
|
240
|
+
const mock = vi.fn();
|
|
241
|
+
mock('test');
|
|
242
|
+
expect(mock).toHaveBeenCalledWith('test');
|
|
444
243
|
```
|
|
445
244
|
|
|
446
|
-
**
|
|
447
|
-
|
|
448
|
-
**Snapshot Best Practices**:
|
|
449
|
-
- Use for UI components, API responses, complex objects
|
|
450
|
-
- Keep snapshots small and focused
|
|
451
|
-
- Review snapshot diffs carefully in PRs
|
|
452
|
-
- Avoid snapshots for simple values (use `.toBe()` instead)
|
|
453
|
-
|
|
454
|
-
### 9. Error Handling Tests
|
|
455
|
-
**Verify Error Conditions**
|
|
456
|
-
|
|
245
|
+
**Stub**: Returns predefined values
|
|
457
246
|
```typescript
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const validator = new ValidationService();
|
|
461
|
-
|
|
462
|
-
expect(() => {
|
|
463
|
-
validator.validate(null);
|
|
464
|
-
}).toThrow('Input is required');
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it('should throw specific error type', () => {
|
|
468
|
-
const validator = new ValidationService();
|
|
469
|
-
|
|
470
|
-
expect(() => {
|
|
471
|
-
validator.validate({ age: -1 });
|
|
472
|
-
}).toThrow(ValidationError);
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it('should include error details', () => {
|
|
476
|
-
const validator = new ValidationService();
|
|
477
|
-
|
|
478
|
-
try {
|
|
479
|
-
validator.validate({ age: 'invalid' });
|
|
480
|
-
fail('Expected error to be thrown');
|
|
481
|
-
} catch (error) {
|
|
482
|
-
expect(error).toBeInstanceOf(ValidationError);
|
|
483
|
-
expect(error.message).toBe('Age must be a number');
|
|
484
|
-
expect(error.field).toBe('age');
|
|
485
|
-
expect(error.code).toBe('INVALID_TYPE');
|
|
486
|
-
}
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it('should handle async errors', async () => {
|
|
490
|
-
const service = new AsyncService();
|
|
491
|
-
|
|
492
|
-
await expect(async () => {
|
|
493
|
-
await service.fetchWithInvalidToken();
|
|
494
|
-
}).rejects.toThrow('Unauthorized');
|
|
495
|
-
});
|
|
496
|
-
});
|
|
247
|
+
const stub = vi.fn().mockReturnValue(42);
|
|
248
|
+
expect(stub()).toBe(42);
|
|
497
249
|
```
|
|
498
250
|
|
|
499
|
-
|
|
500
|
-
|
|
251
|
+
**Spy**: Observes real function
|
|
252
|
+
```typescript
|
|
253
|
+
const spy = vi.spyOn(obj, 'method');
|
|
254
|
+
obj.method();
|
|
255
|
+
expect(spy).toHaveBeenCalled();
|
|
256
|
+
```
|
|
501
257
|
|
|
258
|
+
**Fake**: Working implementation (simplified)
|
|
502
259
|
```typescript
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
async getUser(id: string) {
|
|
506
|
-
const db = new Database(); // Hard-coded dependency
|
|
507
|
-
return db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
260
|
+
class FakeDatabase {
|
|
261
|
+
private data = new Map();
|
|
510
262
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
263
|
+
async save(key, value) {
|
|
264
|
+
this.data.set(key, value);
|
|
265
|
+
}
|
|
514
266
|
|
|
515
|
-
async
|
|
516
|
-
return this.
|
|
267
|
+
async get(key) {
|
|
268
|
+
return this.data.get(key);
|
|
517
269
|
}
|
|
518
270
|
}
|
|
519
|
-
|
|
520
|
-
// Test with mock
|
|
521
|
-
describe('UserService', () => {
|
|
522
|
-
it('should fetch user by id', async () => {
|
|
523
|
-
const mockDb = {
|
|
524
|
-
query: vi.fn().mockResolvedValue({ id: '1', name: 'John' }),
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const service = new UserService(mockDb as any);
|
|
528
|
-
const user = await service.getUser('1');
|
|
529
|
-
|
|
530
|
-
expect(user).toEqual({ id: '1', name: 'John' });
|
|
531
|
-
expect(mockDb.query).toHaveBeenCalledWith(
|
|
532
|
-
'SELECT * FROM users WHERE id = ?',
|
|
533
|
-
['1']
|
|
534
|
-
);
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
271
|
```
|
|
538
272
|
|
|
539
|
-
|
|
273
|
+
---
|
|
540
274
|
|
|
541
|
-
|
|
542
|
-
interface Dependencies {
|
|
543
|
-
logger?: Logger;
|
|
544
|
-
cache?: Cache;
|
|
545
|
-
db?: Database;
|
|
546
|
-
}
|
|
275
|
+
## Coverage Analysis
|
|
547
276
|
|
|
548
|
-
|
|
549
|
-
private logger: Logger;
|
|
550
|
-
private cache: Cache;
|
|
551
|
-
private db: Database;
|
|
552
|
-
|
|
553
|
-
constructor(deps: Dependencies = {}) {
|
|
554
|
-
this.logger = deps.logger ?? new ConsoleLogger();
|
|
555
|
-
this.cache = deps.cache ?? new RedisCache();
|
|
556
|
-
this.db = deps.db ?? new PostgresDatabase();
|
|
557
|
-
}
|
|
558
|
-
}
|
|
277
|
+
### Running Coverage
|
|
559
278
|
|
|
560
|
-
|
|
561
|
-
|
|
279
|
+
```bash
|
|
280
|
+
# Vitest
|
|
281
|
+
vitest --coverage
|
|
562
282
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
logger: silentLogger,
|
|
566
|
-
cache: inMemoryCache,
|
|
567
|
-
db: mockDatabase,
|
|
568
|
-
});
|
|
283
|
+
# Jest
|
|
284
|
+
jest --coverage
|
|
569
285
|
```
|
|
570
286
|
|
|
571
|
-
###
|
|
572
|
-
**Measure and Improve Coverage**
|
|
287
|
+
### Coverage Thresholds
|
|
573
288
|
|
|
574
|
-
```
|
|
289
|
+
```javascript
|
|
575
290
|
// vitest.config.ts
|
|
576
|
-
export default
|
|
291
|
+
export default {
|
|
577
292
|
test: {
|
|
578
293
|
coverage: {
|
|
579
|
-
provider: 'v8',
|
|
580
|
-
reporter: ['text', '
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
thresholds: {
|
|
589
|
-
statements: 80,
|
|
590
|
-
branches: 75,
|
|
591
|
-
functions: 80,
|
|
592
|
-
lines: 80,
|
|
593
|
-
},
|
|
594
|
-
},
|
|
595
|
-
},
|
|
596
|
-
});
|
|
294
|
+
provider: 'v8',
|
|
295
|
+
reporter: ['text', 'html', 'lcov'],
|
|
296
|
+
lines: 80,
|
|
297
|
+
functions: 80,
|
|
298
|
+
branches: 80,
|
|
299
|
+
statements: 80
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
597
303
|
```
|
|
598
304
|
|
|
599
|
-
|
|
305
|
+
### Coverage Best Practices
|
|
600
306
|
|
|
601
|
-
|
|
602
|
-
-
|
|
603
|
-
-
|
|
604
|
-
-
|
|
605
|
-
-
|
|
307
|
+
**✅ DO**:
|
|
308
|
+
- Aim for 80-90% coverage
|
|
309
|
+
- Focus on business logic
|
|
310
|
+
- Test edge cases
|
|
311
|
+
- Test error paths
|
|
606
312
|
|
|
607
|
-
|
|
608
|
-
-
|
|
609
|
-
-
|
|
610
|
-
-
|
|
611
|
-
-
|
|
612
|
-
- Don't game the system (write meaningful tests)
|
|
313
|
+
**❌ DON'T**:
|
|
314
|
+
- Chase 100% coverage
|
|
315
|
+
- Test getters/setters only
|
|
316
|
+
- Test framework code
|
|
317
|
+
- Write tests just for coverage
|
|
613
318
|
|
|
614
|
-
|
|
615
|
-
**Structure for Maintainability**
|
|
319
|
+
---
|
|
616
320
|
|
|
617
|
-
|
|
618
|
-
src/
|
|
619
|
-
├── services/
|
|
620
|
-
│ ├── UserService.ts
|
|
621
|
-
│ ├── UserService.test.ts # Co-located tests
|
|
622
|
-
│ ├── OrderService.ts
|
|
623
|
-
│ └── OrderService.test.ts
|
|
624
|
-
├── utils/
|
|
625
|
-
│ ├── validation.ts
|
|
626
|
-
│ ├── validation.test.ts
|
|
627
|
-
│ ├── formatting.ts
|
|
628
|
-
│ └── formatting.test.ts
|
|
629
|
-
└── __tests__/ # Alternative: separate test dir
|
|
630
|
-
├── unit/
|
|
631
|
-
│ ├── services/
|
|
632
|
-
│ │ ├── UserService.test.ts
|
|
633
|
-
│ │ └── OrderService.test.ts
|
|
634
|
-
│ └── utils/
|
|
635
|
-
│ ├── validation.test.ts
|
|
636
|
-
│ └── formatting.test.ts
|
|
637
|
-
└── integration/
|
|
638
|
-
├── api.test.ts
|
|
639
|
-
└── database.test.ts
|
|
640
|
-
```
|
|
321
|
+
## Snapshot Testing
|
|
641
322
|
|
|
642
|
-
|
|
643
|
-
- Test files: `*.test.ts` or `*.spec.ts`
|
|
644
|
-
- Test suites: `describe('ClassName')`
|
|
645
|
-
- Test cases: `it('should do something specific')`
|
|
646
|
-
- Helper files: `*.fixture.ts`, `*.mock.ts`
|
|
323
|
+
### When to Use Snapshots
|
|
647
324
|
|
|
648
|
-
|
|
649
|
-
|
|
325
|
+
**Good use cases**:
|
|
326
|
+
- UI component output
|
|
327
|
+
- API responses
|
|
328
|
+
- Configuration objects
|
|
329
|
+
- Error messages
|
|
650
330
|
|
|
651
331
|
```typescript
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
name: 'John Doe',
|
|
656
|
-
email: 'john@example.com',
|
|
657
|
-
createdAt: new Date('2025-01-01'),
|
|
658
|
-
...overrides,
|
|
332
|
+
it('should render user card', () => {
|
|
333
|
+
const card = renderUserCard({ name: 'John', role: 'Admin' });
|
|
334
|
+
expect(card).toMatchSnapshot();
|
|
659
335
|
});
|
|
660
336
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
createUser({ id: String(i + 1), name: `User ${i + 1}` })
|
|
664
|
-
);
|
|
337
|
+
// Update snapshots: vitest -u
|
|
338
|
+
```
|
|
665
339
|
|
|
666
|
-
|
|
667
|
-
|
|
340
|
+
**Avoid snapshots for**:
|
|
341
|
+
- Dates/timestamps
|
|
342
|
+
- Random values
|
|
343
|
+
- Large objects (prefer specific assertions)
|
|
668
344
|
|
|
669
|
-
|
|
670
|
-
it('should save user', async () => {
|
|
671
|
-
const user = createUser({ name: 'Jane' });
|
|
672
|
-
await repo.save(user);
|
|
345
|
+
---
|
|
673
346
|
|
|
674
|
-
|
|
675
|
-
});
|
|
347
|
+
## Test Organization
|
|
676
348
|
|
|
677
|
-
|
|
678
|
-
const users = createUserList(5);
|
|
679
|
-
await Promise.all(users.map(u => repo.save(u)));
|
|
349
|
+
### File Structure
|
|
680
350
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
351
|
+
```
|
|
352
|
+
src/
|
|
353
|
+
├── services/
|
|
354
|
+
│ ├── UserService.ts
|
|
355
|
+
│ └── UserService.test.ts ← Co-located
|
|
356
|
+
tests/
|
|
357
|
+
├── unit/
|
|
358
|
+
│ └── utils.test.ts
|
|
359
|
+
├── integration/
|
|
360
|
+
│ └── api.test.ts
|
|
361
|
+
└── fixtures/
|
|
362
|
+
└── users.json
|
|
684
363
|
```
|
|
685
364
|
|
|
686
|
-
|
|
365
|
+
### Test Naming
|
|
687
366
|
|
|
367
|
+
**✅ GOOD**:
|
|
688
368
|
```typescript
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const interval = setInterval(() => {
|
|
694
|
-
if (condition()) {
|
|
695
|
-
clearInterval(interval);
|
|
696
|
-
resolve(true);
|
|
697
|
-
} else if (Date.now() - startTime > timeout) {
|
|
698
|
-
clearInterval(interval);
|
|
699
|
-
reject(new Error('Timeout waiting for condition'));
|
|
700
|
-
}
|
|
701
|
-
}, 10);
|
|
702
|
-
});
|
|
703
|
-
};
|
|
704
|
-
|
|
705
|
-
export const flushPromises = () => new Promise(resolve => setImmediate(resolve));
|
|
706
|
-
|
|
707
|
-
export const createMockLogger = () => ({
|
|
708
|
-
info: vi.fn(),
|
|
709
|
-
warn: vi.fn(),
|
|
710
|
-
error: vi.fn(),
|
|
711
|
-
debug: vi.fn(),
|
|
369
|
+
describe('UserService.create', () => {
|
|
370
|
+
it('should create user with valid email', () => {});
|
|
371
|
+
it('should throw error for invalid email', () => {});
|
|
372
|
+
it('should generate unique ID', () => {});
|
|
712
373
|
});
|
|
713
374
|
```
|
|
714
375
|
|
|
715
|
-
|
|
716
|
-
**Custom and Built-in Matchers**
|
|
717
|
-
|
|
376
|
+
**❌ BAD**:
|
|
718
377
|
```typescript
|
|
719
|
-
describe('
|
|
720
|
-
|
|
721
|
-
it('
|
|
722
|
-
it('deep equality', () => expect({ a: 1 }).toEqual({ a: 1 }));
|
|
723
|
-
it('reference equality', () => {
|
|
724
|
-
const obj = { a: 1 };
|
|
725
|
-
expect(obj).toBe(obj);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
// Truthiness
|
|
729
|
-
it('truthy', () => expect(true).toBeTruthy());
|
|
730
|
-
it('falsy', () => expect(false).toBeFalsy());
|
|
731
|
-
it('defined', () => expect('value').toBeDefined());
|
|
732
|
-
it('undefined', () => expect(undefined).toBeUndefined());
|
|
733
|
-
it('null', () => expect(null).toBeNull());
|
|
734
|
-
|
|
735
|
-
// Numbers
|
|
736
|
-
it('greater than', () => expect(10).toBeGreaterThan(5));
|
|
737
|
-
it('less than', () => expect(5).toBeLessThan(10));
|
|
738
|
-
it('close to', () => expect(0.1 + 0.2).toBeCloseTo(0.3, 5));
|
|
739
|
-
|
|
740
|
-
// Strings
|
|
741
|
-
it('contains', () => expect('hello world').toContain('world'));
|
|
742
|
-
it('matches regex', () => expect('test@example.com').toMatch(/^\S+@\S+$/));
|
|
743
|
-
|
|
744
|
-
// Arrays
|
|
745
|
-
it('contains item', () => expect([1, 2, 3]).toContain(2));
|
|
746
|
-
it('has length', () => expect([1, 2, 3]).toHaveLength(3));
|
|
747
|
-
it('contains object', () => {
|
|
748
|
-
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 });
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
// Objects
|
|
752
|
-
it('matches object', () => {
|
|
753
|
-
expect({ id: 1, name: 'John', age: 30 }).toMatchObject({
|
|
754
|
-
id: 1,
|
|
755
|
-
name: 'John',
|
|
756
|
-
});
|
|
757
|
-
});
|
|
758
|
-
it('has property', () => expect({ a: 1 }).toHaveProperty('a'));
|
|
759
|
-
it('has property with value', () => expect({ a: 1 }).toHaveProperty('a', 1));
|
|
760
|
-
|
|
761
|
-
// Functions
|
|
762
|
-
it('throws', () => {
|
|
763
|
-
expect(() => { throw new Error('fail'); }).toThrow('fail');
|
|
764
|
-
});
|
|
765
|
-
it('called', () => {
|
|
766
|
-
const mock = vi.fn();
|
|
767
|
-
mock();
|
|
768
|
-
expect(mock).toHaveBeenCalled();
|
|
769
|
-
});
|
|
770
|
-
it('called with', () => {
|
|
771
|
-
const mock = vi.fn();
|
|
772
|
-
mock(1, 2);
|
|
773
|
-
expect(mock).toHaveBeenCalledWith(1, 2);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
// Negation
|
|
777
|
-
it('not equal', () => expect(1).not.toBe(2));
|
|
378
|
+
describe('UserService', () => {
|
|
379
|
+
it('test1', () => {});
|
|
380
|
+
it('should work', () => {});
|
|
778
381
|
});
|
|
779
382
|
```
|
|
780
383
|
|
|
781
|
-
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Error Handling Tests
|
|
782
387
|
|
|
783
388
|
```typescript
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
expect.
|
|
787
|
-
toBeValidEmail(received: string) {
|
|
788
|
-
const pass = /^\S+@\S+\.\S+$/.test(received);
|
|
789
|
-
return {
|
|
790
|
-
pass,
|
|
791
|
-
message: () =>
|
|
792
|
-
pass
|
|
793
|
-
? `expected ${received} not to be a valid email`
|
|
794
|
-
: `expected ${received} to be a valid email`,
|
|
795
|
-
};
|
|
796
|
-
},
|
|
389
|
+
// Synchronous errors
|
|
390
|
+
it('should throw for negative numbers', () => {
|
|
391
|
+
expect(() => sqrt(-1)).toThrow('Cannot compute square root of negative');
|
|
797
392
|
});
|
|
798
393
|
|
|
799
|
-
//
|
|
800
|
-
it('should
|
|
801
|
-
expect('
|
|
802
|
-
expect('invalid').not.toBeValidEmail();
|
|
394
|
+
// Async errors
|
|
395
|
+
it('should reject for invalid ID', async () => {
|
|
396
|
+
await expect(fetchUser('invalid')).rejects.toThrow('Invalid ID');
|
|
803
397
|
});
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
### 15. Test Lifecycle Hooks
|
|
807
|
-
**Setup and Teardown**
|
|
808
|
-
|
|
809
|
-
```typescript
|
|
810
|
-
describe('Database Tests', () => {
|
|
811
|
-
let db: Database;
|
|
812
|
-
|
|
813
|
-
// Run once before all tests in suite
|
|
814
|
-
beforeAll(async () => {
|
|
815
|
-
db = new Database();
|
|
816
|
-
await db.connect();
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
// Run once after all tests in suite
|
|
820
|
-
afterAll(async () => {
|
|
821
|
-
await db.disconnect();
|
|
822
|
-
});
|
|
823
398
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
// Run after each test
|
|
831
|
-
afterEach(async () => {
|
|
832
|
-
await db.rollback();
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
it('should insert user', async () => {
|
|
836
|
-
await db.insert('users', { name: 'John' });
|
|
837
|
-
const users = await db.query('users');
|
|
838
|
-
expect(users).toHaveLength(1);
|
|
839
|
-
});
|
|
399
|
+
// Error types
|
|
400
|
+
it('should throw TypeError', () => {
|
|
401
|
+
expect(() => doSomething()).toThrow(TypeError);
|
|
402
|
+
});
|
|
840
403
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const users = await db.query('users');
|
|
845
|
-
expect(users).toHaveLength(0);
|
|
846
|
-
});
|
|
404
|
+
// Custom errors
|
|
405
|
+
it('should throw ValidationError', () => {
|
|
406
|
+
expect(() => validate()).toThrow(ValidationError);
|
|
847
407
|
});
|
|
848
408
|
```
|
|
849
409
|
|
|
850
|
-
|
|
410
|
+
---
|
|
851
411
|
|
|
852
|
-
|
|
853
|
-
// Skip tests
|
|
854
|
-
it.skip('not ready yet', () => {});
|
|
855
|
-
it.todo('implement later');
|
|
412
|
+
## Test Isolation
|
|
856
413
|
|
|
857
|
-
|
|
858
|
-
it.only('focus on this test', () => {});
|
|
414
|
+
### Reset State Between Tests
|
|
859
415
|
|
|
860
|
-
|
|
861
|
-
|
|
416
|
+
```typescript
|
|
417
|
+
let service: UserService;
|
|
862
418
|
|
|
863
|
-
|
|
864
|
-
|
|
419
|
+
beforeEach(() => {
|
|
420
|
+
service = new UserService();
|
|
421
|
+
vi.clearAllMocks();
|
|
422
|
+
});
|
|
865
423
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
it('test 1', async () => { /* runs in parallel */ });
|
|
869
|
-
it('test 2', async () => { /* runs in parallel */ });
|
|
424
|
+
afterEach(() => {
|
|
425
|
+
vi.restoreAllMocks();
|
|
870
426
|
});
|
|
871
427
|
```
|
|
872
428
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
### Test Isolation
|
|
876
|
-
- Each test should be independent
|
|
877
|
-
- No shared state between tests
|
|
878
|
-
- Use `beforeEach` to reset state
|
|
879
|
-
- Avoid global variables
|
|
880
|
-
- Clean up resources in `afterEach`
|
|
881
|
-
|
|
882
|
-
### Test Naming
|
|
883
|
-
- Use descriptive names: `it('should return user when id exists')`
|
|
884
|
-
- Follow "should" convention
|
|
885
|
-
- Be specific about what is being tested
|
|
886
|
-
- Include edge cases in name: `it('should handle empty array')`
|
|
887
|
-
|
|
888
|
-
### Avoid Test Smells
|
|
889
|
-
❌ **Don't**:
|
|
890
|
-
- Test implementation details
|
|
891
|
-
- Write slow tests (mock external deps)
|
|
892
|
-
- Use magic numbers (use constants)
|
|
893
|
-
- Share state between tests
|
|
894
|
-
- Test framework code (test YOUR code)
|
|
429
|
+
### Avoid Test Interdependence
|
|
895
430
|
|
|
896
|
-
|
|
897
|
-
- Test behavior, not implementation
|
|
898
|
-
- Keep tests fast (< 100ms per test)
|
|
899
|
-
- Use descriptive variable names
|
|
900
|
-
- Isolate tests completely
|
|
901
|
-
- Focus on edge cases and error paths
|
|
902
|
-
|
|
903
|
-
### Performance
|
|
904
|
-
- Mock expensive operations (DB, API, file I/O)
|
|
905
|
-
- Use fake timers for time-based code
|
|
906
|
-
- Run tests in parallel (`--threads`)
|
|
907
|
-
- Cache test fixtures
|
|
908
|
-
- Profile slow tests: `npm test -- --reporter=verbose`
|
|
909
|
-
|
|
910
|
-
## Common Patterns
|
|
911
|
-
|
|
912
|
-
### Testing Classes
|
|
431
|
+
**❌ BAD**:
|
|
913
432
|
```typescript
|
|
914
|
-
|
|
915
|
-
private count = 0;
|
|
916
|
-
|
|
917
|
-
increment() { this.count++; }
|
|
918
|
-
decrement() { this.count--; }
|
|
919
|
-
getValue() { return this.count; }
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
describe('Counter', () => {
|
|
923
|
-
let counter: Counter;
|
|
924
|
-
|
|
925
|
-
beforeEach(() => {
|
|
926
|
-
counter = new Counter();
|
|
927
|
-
});
|
|
433
|
+
let user;
|
|
928
434
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
it('should increment', () => {
|
|
934
|
-
counter.increment();
|
|
935
|
-
expect(counter.getValue()).toBe(1);
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
it('should decrement', () => {
|
|
939
|
-
counter.decrement();
|
|
940
|
-
expect(counter.getValue()).toBe(-1);
|
|
941
|
-
});
|
|
435
|
+
it('should create user', () => {
|
|
436
|
+
user = createUser(); // Shared state
|
|
942
437
|
});
|
|
943
|
-
```
|
|
944
438
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
describe('formatCurrency', () => {
|
|
948
|
-
it.each([
|
|
949
|
-
[1000, '$1,000.00'],
|
|
950
|
-
[0.5, '$0.50'],
|
|
951
|
-
[-100, '-$100.00'],
|
|
952
|
-
])('formatCurrency(%i) should return %s', (input, expected) => {
|
|
953
|
-
expect(formatCurrency(input)).toBe(expected);
|
|
954
|
-
});
|
|
439
|
+
it('should update user', () => {
|
|
440
|
+
updateUser(user); // Depends on previous test
|
|
955
441
|
});
|
|
956
442
|
```
|
|
957
443
|
|
|
958
|
-
|
|
444
|
+
**✅ GOOD**:
|
|
959
445
|
```typescript
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
it('should increment', () => {
|
|
965
|
-
const { result } = renderHook(() => useCounter());
|
|
966
|
-
|
|
967
|
-
act(() => {
|
|
968
|
-
result.current.increment();
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
expect(result.current.count).toBe(1);
|
|
972
|
-
});
|
|
446
|
+
it('should update user', () => {
|
|
447
|
+
const user = createUser();
|
|
448
|
+
updateUser(user);
|
|
449
|
+
expect(user.updated).toBe(true);
|
|
973
450
|
});
|
|
974
451
|
```
|
|
975
452
|
|
|
976
|
-
|
|
453
|
+
---
|
|
977
454
|
|
|
978
|
-
|
|
979
|
-
1. **Timeouts**: Increase timeout for slow async operations
|
|
980
|
-
2. **Flaky tests**: Ensure proper cleanup, avoid race conditions
|
|
981
|
-
3. **Mock not working**: Check mock is hoisted, correct path
|
|
982
|
-
4. **Coverage gaps**: Use `--coverage` to identify untested code
|
|
983
|
-
5. **Slow tests**: Profile and mock expensive operations
|
|
455
|
+
## Best Practices Summary
|
|
984
456
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
457
|
+
**✅ DO**:
|
|
458
|
+
- Write tests before code (TDD)
|
|
459
|
+
- Test behavior, not implementation
|
|
460
|
+
- One assertion per test (when possible)
|
|
461
|
+
- Clear test names (should...)
|
|
462
|
+
- Mock external dependencies
|
|
463
|
+
- Test edge cases and errors
|
|
464
|
+
- Keep tests fast (<100ms each)
|
|
465
|
+
- Use descriptive variable names
|
|
466
|
+
- Clean up after tests
|
|
989
467
|
|
|
990
|
-
|
|
991
|
-
|
|
468
|
+
**❌ DON'T**:
|
|
469
|
+
- Test private methods directly
|
|
470
|
+
- Share state between tests
|
|
471
|
+
- Use real databases/APIs
|
|
472
|
+
- Test framework code
|
|
473
|
+
- Write fragile tests (implementation-dependent)
|
|
474
|
+
- Skip error cases
|
|
475
|
+
- Use magic numbers
|
|
476
|
+
- Leave commented-out tests
|
|
992
477
|
|
|
993
|
-
|
|
994
|
-
node --inspect-brk node_modules/.bin/vitest run
|
|
478
|
+
---
|
|
995
479
|
|
|
996
|
-
|
|
997
|
-
npm test -- --watch
|
|
480
|
+
## Quick Reference
|
|
998
481
|
|
|
999
|
-
|
|
1000
|
-
|
|
482
|
+
### Assertions
|
|
483
|
+
```typescript
|
|
484
|
+
expect(value).toBe(expected); // ===
|
|
485
|
+
expect(value).toEqual(expected); // Deep equality
|
|
486
|
+
expect(value).toBeTruthy(); // Boolean true
|
|
487
|
+
expect(value).toBeFalsy(); // Boolean false
|
|
488
|
+
expect(array).toHaveLength(3); // Array length
|
|
489
|
+
expect(array).toContain(item); // Array includes
|
|
490
|
+
expect(string).toMatch(/pattern/); // Regex match
|
|
491
|
+
expect(fn).toThrow(Error); // Throws error
|
|
492
|
+
expect(obj).toHaveProperty('key'); // Has property
|
|
493
|
+
expect(value).toBeCloseTo(0.3, 5); // Float comparison
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Lifecycle Hooks
|
|
497
|
+
```typescript
|
|
498
|
+
beforeAll(() => {}); // Once before all tests
|
|
499
|
+
beforeEach(() => {}); // Before each test
|
|
500
|
+
afterEach(() => {}); // After each test
|
|
501
|
+
afterAll(() => {}); // Once after all tests
|
|
502
|
+
```
|
|
1001
503
|
|
|
1002
|
-
|
|
1003
|
-
|
|
504
|
+
### Mock Utilities
|
|
505
|
+
```typescript
|
|
506
|
+
vi.fn() // Create mock
|
|
507
|
+
vi.fn().mockReturnValue(x) // Return value
|
|
508
|
+
vi.fn().mockResolvedValue(x) // Async return
|
|
509
|
+
vi.fn().mockRejectedValue(e) // Async error
|
|
510
|
+
vi.mock('./module') // Mock module
|
|
511
|
+
vi.spyOn(obj, 'method') // Spy on method
|
|
512
|
+
vi.clearAllMocks() // Clear call history
|
|
513
|
+
vi.resetAllMocks() // Reset + clear
|
|
514
|
+
vi.restoreAllMocks() // Restore originals
|
|
1004
515
|
```
|
|
1005
516
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
-
|
|
1009
|
-
- **Jest API**: https://jestjs.io/docs/api (Jest-compatible)
|
|
1010
|
-
- **TDD Guide**: https://martinfowler.com/bliki/TestDrivenDevelopment.html
|
|
1011
|
-
- **Test Doubles**: https://martinfowler.com/bliki/TestDouble.html
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
**This skill is self-contained and works in ANY user project with Vitest/Jest.**
|