specweave 0.26.4 → 0.26.9

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.
Files changed (82) hide show
  1. package/CLAUDE.md +154 -4
  2. package/bin/specweave.js +15 -0
  3. package/dist/plugins/specweave-github/lib/completion-calculator.js +2 -2
  4. package/dist/plugins/specweave-github/lib/completion-calculator.js.map +1 -1
  5. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts +28 -1
  6. package/dist/plugins/specweave-github/lib/github-feature-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-github/lib/github-feature-sync.js +191 -19
  8. package/dist/plugins/specweave-github/lib/github-feature-sync.js.map +1 -1
  9. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts +3 -0
  10. package/dist/plugins/specweave-github/lib/user-story-issue-builder.d.ts.map +1 -1
  11. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js +25 -2
  12. package/dist/plugins/specweave-github/lib/user-story-issue-builder.js.map +1 -1
  13. package/dist/src/cli/commands/archive.d.ts +10 -0
  14. package/dist/src/cli/commands/archive.d.ts.map +1 -0
  15. package/dist/src/cli/commands/archive.js +78 -0
  16. package/dist/src/cli/commands/archive.js.map +1 -0
  17. package/dist/src/cli/commands/init.js +2 -2
  18. package/dist/src/cli/commands/init.js.map +1 -1
  19. package/dist/src/cli/helpers/init/initial-increment-generator.d.ts.map +1 -1
  20. package/dist/src/cli/helpers/init/initial-increment-generator.js +48 -8
  21. package/dist/src/cli/helpers/init/initial-increment-generator.js.map +1 -1
  22. package/dist/src/core/increment/increment-reopener.d.ts.map +1 -1
  23. package/dist/src/core/increment/increment-reopener.js +13 -14
  24. package/dist/src/core/increment/increment-reopener.js.map +1 -1
  25. package/dist/src/core/increment/metadata-manager.d.ts.map +1 -1
  26. package/dist/src/core/increment/metadata-manager.js +19 -0
  27. package/dist/src/core/increment/metadata-manager.js.map +1 -1
  28. package/dist/src/core/increment/status-change-sync-trigger.d.ts +85 -0
  29. package/dist/src/core/increment/status-change-sync-trigger.d.ts.map +1 -0
  30. package/dist/src/core/increment/status-change-sync-trigger.js +137 -0
  31. package/dist/src/core/increment/status-change-sync-trigger.js.map +1 -0
  32. package/dist/src/core/increment/sync-circuit-breaker.d.ts +64 -0
  33. package/dist/src/core/increment/sync-circuit-breaker.d.ts.map +1 -0
  34. package/dist/src/core/increment/sync-circuit-breaker.js +95 -0
  35. package/dist/src/core/increment/sync-circuit-breaker.js.map +1 -0
  36. package/dist/src/core/living-docs/living-docs-sync.d.ts +12 -0
  37. package/dist/src/core/living-docs/living-docs-sync.d.ts.map +1 -1
  38. package/dist/src/core/living-docs/living-docs-sync.js +157 -24
  39. package/dist/src/core/living-docs/living-docs-sync.js.map +1 -1
  40. package/dist/src/init/repo/types.d.ts +1 -1
  41. package/package.json +2 -2
  42. package/plugins/specweave/agents/pm/AGENT.md +13 -7
  43. package/plugins/specweave/commands/sync-diagnostics.md +227 -0
  44. package/plugins/specweave/hooks/docs-changed.sh.backup +79 -0
  45. package/plugins/specweave/hooks/human-input-required.sh.backup +75 -0
  46. package/plugins/specweave/hooks/post-first-increment.sh.backup +61 -0
  47. package/plugins/specweave/hooks/post-increment-change.sh.backup +98 -0
  48. package/plugins/specweave/hooks/post-increment-completion.sh.backup +231 -0
  49. package/plugins/specweave/hooks/post-increment-planning.sh.backup +1048 -0
  50. package/plugins/specweave/hooks/post-increment-status-change.sh.backup +147 -0
  51. package/plugins/specweave/hooks/post-spec-update.sh.backup +158 -0
  52. package/plugins/specweave/hooks/post-user-story-complete.sh.backup +179 -0
  53. package/plugins/specweave/hooks/pre-command-deduplication.sh.backup +83 -0
  54. package/plugins/specweave/hooks/pre-implementation.sh.backup +67 -0
  55. package/plugins/specweave/hooks/pre-task-completion.sh.backup +194 -0
  56. package/plugins/specweave/hooks/pre-tool-use.sh.backup +133 -0
  57. package/plugins/specweave/hooks/user-prompt-submit.sh +20 -8
  58. package/plugins/specweave/hooks/user-prompt-submit.sh.backup +386 -0
  59. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js +19 -0
  60. package/plugins/specweave/lib/vendor/core/increment/metadata-manager.js.map +1 -1
  61. package/plugins/specweave/skills/brownfield-analyzer/SKILL.md +267 -868
  62. package/plugins/specweave/skills/increment-planner/SKILL.md +379 -1245
  63. package/plugins/specweave/skills/role-orchestrator/SKILL.md +293 -969
  64. package/plugins/specweave-ado/hooks/post-living-docs-update.sh.backup +353 -0
  65. package/plugins/specweave-ado/hooks/post-task-completion.sh.backup +172 -0
  66. package/plugins/specweave-ado/lib/ado-multi-project-sync.js +1 -0
  67. package/plugins/specweave-ado/lib/enhanced-ado-sync.js +170 -0
  68. package/plugins/specweave-docs/skills/technical-writing/SKILL.md +333 -839
  69. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +1080 -0
  70. package/plugins/specweave-github/hooks/post-task-completion.sh.backup +258 -0
  71. package/plugins/specweave-github/lib/completion-calculator.js +1 -1
  72. package/plugins/specweave-github/lib/completion-calculator.ts +2 -2
  73. package/plugins/specweave-github/lib/github-feature-sync.js +152 -18
  74. package/plugins/specweave-github/lib/github-feature-sync.ts +225 -22
  75. package/plugins/specweave-github/lib/user-story-issue-builder.js +21 -1
  76. package/plugins/specweave-github/lib/user-story-issue-builder.ts +31 -3
  77. package/plugins/specweave-jira/hooks/post-task-completion.sh.backup +172 -0
  78. package/plugins/specweave-jira/lib/enhanced-jira-sync.js +3 -3
  79. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +981 -0
  80. package/plugins/specweave-release/hooks/post-task-completion.sh.backup +110 -0
  81. package/plugins/specweave-testing/skills/tdd-expert/SKILL.md +269 -749
  82. 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
- ## Core Expertise
8
+ **Self-contained unit testing expertise for Vitest/Jest in ANY user project.**
9
9
 
10
- ### 1. Vitest Fundamentals
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
- it('should throw error for invalid email', () => {
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
- ### 2. Test-Driven Development (TDD)
48
- **Red-Green-Refactor Cycle**
14
+ **Red-Green-Refactor Cycle**:
49
15
 
50
16
  ```typescript
51
- // RED: Write failing test first
17
+ // 1. RED: Write failing test
52
18
  describe('Calculator', () => {
53
19
  it('should add two numbers', () => {
54
- const calculator = new Calculator();
55
- expect(calculator.add(2, 3)).toBe(5);
20
+ const calc = new Calculator();
21
+ expect(calc.add(2, 3)).toBe(5);
56
22
  });
57
23
  });
58
24
 
59
- // GREEN: Implement minimal code to pass
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 without breaking tests
32
+ // 3. REFACTOR: Improve code
67
33
  class Calculator {
68
34
  add(...numbers: number[]): number {
69
- return numbers.reduce((sum, num) => sum + num, 0);
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
- - Forces modular, testable design
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
- ### 3. AAA Pattern (Arrange-Act-Assert)
88
- **Structure for Clear Tests**
46
+ ---
89
47
 
90
- ```typescript
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
- **Alternative: Given-When-Then (BDD Style)**
50
+ ### Basic Test Structure
113
51
 
114
52
  ```typescript
115
- describe('OrderService', () => {
116
- it('should apply discount when valid code is provided', () => {
117
- // GIVEN: An order with items and a discount code
118
- const orderService = new OrderService();
119
- const order = createOrder({ discountCode: 'SAVE20' });
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
- // WHEN: Calculating the total
122
- const total = orderService.calculateTotal(order);
59
+ beforeEach(() => {
60
+ service = new UserService();
61
+ });
123
62
 
124
- // THEN: The discount should be applied
125
- expect(total).toBe(200);
63
+ afterEach(() => {
64
+ vi.clearAllMocks();
126
65
  });
127
- });
128
- ```
129
66
 
130
- ### 4. Mocking Strategies
131
- **Test Doubles: Mocks, Stubs, Spies, Fakes**
67
+ it('should create user', () => {
68
+ const user = service.create({ name: 'John', email: 'john@test.com' });
132
69
 
133
- #### Mocks (Track Calls + Control Behavior)
134
- ```typescript
135
- import { vi } from 'vitest';
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
- #### Spies (Track Calls on Real Methods)
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
- expect(consoleErrorSpy).toHaveBeenCalledWith('[ERROR]', 'Something went wrong');
87
+ ```typescript
88
+ it('should fetch user from API', async () => {
89
+ const user = await api.fetchUser('user-123');
166
90
 
167
- consoleErrorSpy.mockRestore();
91
+ expect(user).toEqual({
92
+ id: 'user-123',
93
+ name: 'John Doe'
168
94
  });
169
95
  });
170
- ```
171
96
 
172
- #### Stubs (Return Predefined Values)
173
- ```typescript
174
- describe('UserRepository', () => {
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
- #### Fakes (Working Implementations for Testing)
192
- ```typescript
193
- // Fake in-memory database
194
- class FakeDatabase {
195
- private data: Map<string, any> = new Map();
103
+ ---
196
104
 
197
- async save(key: string, value: any): Promise<void> {
198
- this.data.set(key, value);
199
- }
105
+ ## Mocking Strategies
200
106
 
201
- async find(key: string): Promise<any> {
202
- return this.data.get(key);
203
- }
107
+ ### 1. Mock Functions
204
108
 
205
- async delete(key: string): Promise<void> {
206
- this.data.delete(key);
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
- await cache.set('key', 'value');
216
- const result = await cache.get('key');
115
+ // Mock with implementation
116
+ const mockAdd = vi.fn((a, b) => a + b);
117
+ expect(mockAdd(2, 3)).toBe(5);
217
118
 
218
- expect(result).toBe('value');
219
- });
220
- });
119
+ // Verify calls
120
+ expect(mockFn).toHaveBeenCalledTimes(1);
121
+ expect(mockFn).toHaveBeenCalledWith(expected);
221
122
  ```
222
123
 
223
- ### 5. Module Mocking
224
- **Mock Entire Modules**
124
+ ### 2. Mock Modules
225
125
 
226
126
  ```typescript
227
- // Mock external dependency
127
+ // Mock entire module
228
128
  vi.mock('./database', () => ({
229
- Database: vi.fn().mockImplementation(() => ({
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 { Database } from './database';
237
- import { UserService } from './UserService';
132
+ import { query } from './database';
238
133
 
239
- describe('UserService', () => {
240
- it('should connect to database on initialization', async () => {
241
- const userService = new UserService();
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
- **Partial Module Mocking**
140
+ ### 3. Spies
250
141
 
251
142
  ```typescript
252
- vi.mock('./utils', async (importOriginal) => {
253
- const actual = await importOriginal();
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
- **Auto-mocking with vi.hoisted**
146
+ myFunction();
263
147
 
264
- ```typescript
265
- import { vi } from 'vitest';
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
- ### 6. Async Testing
278
- **Handle Promises, Timers, and Callbacks**
152
+ ### 4. Mock Dependencies
279
153
 
280
- #### Testing Promises
281
154
  ```typescript
282
- describe('AsyncService', () => {
283
- it('should resolve with data', async () => {
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
- await expect(service.fetchInvalidData()).rejects.toThrow('Not found');
295
- });
158
+ async getUser(id: string) {
159
+ return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
160
+ }
161
+ }
296
162
 
297
- it('should handle multiple async operations', async () => {
298
- const service = new AsyncService();
163
+ // Test with mock
164
+ const mockDb = {
165
+ query: vi.fn().mockResolvedValue({ id: '123', name: 'John' })
166
+ };
299
167
 
300
- const [user, posts] = await Promise.all([
301
- service.fetchUser(1),
302
- service.fetchPosts(1),
303
- ]);
168
+ const service = new UserService(mockDb);
169
+ const user = await service.getUser('123');
304
170
 
305
- expect(user.id).toBe(1);
306
- expect(posts.length).toBeGreaterThan(0);
307
- });
308
- });
171
+ expect(mockDb.query).toHaveBeenCalledWith(
172
+ 'SELECT * FROM users WHERE id = ?',
173
+ ['123']
174
+ );
309
175
  ```
310
176
 
311
- #### Testing Timers
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
- expect(callback).toHaveBeenCalledTimes(1);
336
- });
179
+ ## Test Patterns
337
180
 
338
- it('should cancel pending debounced calls', () => {
339
- const callback = vi.fn();
340
- const debounced = debounce(callback, 1000);
181
+ ### AAA Pattern (Arrange-Act-Assert)
341
182
 
342
- debounced();
343
- debounced.cancel();
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
- vi.advanceTimersByTime(1000);
190
+ // Act
191
+ const total = cart.getTotal();
346
192
 
347
- expect(callback).not.toHaveBeenCalled();
348
- });
193
+ // Assert
194
+ expect(total).toBe(35);
349
195
  });
350
196
  ```
351
197
 
352
- #### Testing Callbacks
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
- emitter.emit('data', 'test');
364
- });
365
-
366
- // Modern alternative: Promisify
367
- it('should execute callback on event (promisified)', () => {
368
- const emitter = new EventEmitter();
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
- emitter.emit('data', 'test');
207
+ // When: Getting the total
208
+ const total = cart.getTotal();
375
209
 
376
- return expect(promise).resolves.toBe('test');
210
+ // Then: 10% discount applied
211
+ expect(total).toBe(108); // $120 - $12 (10%)
377
212
  });
378
213
  });
379
214
  ```
380
215
 
381
- ### 7. Parametric Testing (Table-Driven Tests)
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
- [3, 4, 7],
401
- ])('add(%i, %i) should equal %i', (a, b, expected) => {
402
- expect(add(a, b)).toBe(expected);
403
- });
404
-
405
- // Complex validation
406
- describe.each([
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
- ### 8. Snapshot Testing
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
- expect(rendered).toMatchSnapshot();
435
- });
234
+ ## Test Doubles
436
235
 
437
- // Inline snapshots (better for small outputs)
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
- expect(formatted).toMatchInlineSnapshot('"January 15, 2025"');
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
- **Update snapshots**: `npm test -- -u`
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
- describe('ValidationService', () => {
459
- it('should throw for invalid input', () => {
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
- ### 10. Dependency Injection for Testability
500
- **Design for Easy Testing**
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
- // BAD: Hard to test (tight coupling)
504
- class UserService {
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
- // ✅ GOOD: Easy to test (dependency injection)
512
- class UserService {
513
- constructor(private db: Database) {}
263
+ async save(key, value) {
264
+ this.data.set(key, value);
265
+ }
514
266
 
515
- async getUser(id: string) {
516
- return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
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
- **Factory Pattern for Dependencies**
273
+ ---
540
274
 
541
- ```typescript
542
- interface Dependencies {
543
- logger?: Logger;
544
- cache?: Cache;
545
- db?: Database;
546
- }
275
+ ## Coverage Analysis
547
276
 
548
- class UserService {
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
- // Production
561
- const service = new UserService();
279
+ ```bash
280
+ # Vitest
281
+ vitest --coverage
562
282
 
563
- // Testing
564
- const service = new UserService({
565
- logger: silentLogger,
566
- cache: inMemoryCache,
567
- db: mockDatabase,
568
- });
283
+ # Jest
284
+ jest --coverage
569
285
  ```
570
286
 
571
- ### 11. Test Coverage Analysis
572
- **Measure and Improve Coverage**
287
+ ### Coverage Thresholds
573
288
 
574
- ```typescript
289
+ ```javascript
575
290
  // vitest.config.ts
576
- export default defineConfig({
291
+ export default {
577
292
  test: {
578
293
  coverage: {
579
- provider: 'v8', // or 'istanbul'
580
- reporter: ['text', 'json', 'html', 'lcov'],
581
- include: ['src/**/*.ts'],
582
- exclude: [
583
- 'src/**/*.test.ts',
584
- 'src/**/*.spec.ts',
585
- 'src/types/**',
586
- 'src/index.ts',
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
- **Run with coverage**: `npm test -- --coverage`
305
+ ### Coverage Best Practices
600
306
 
601
- **Coverage Types**:
602
- - **Statement**: % of code statements executed
603
- - **Branch**: % of conditional branches tested
604
- - **Function**: % of functions called
605
- - **Line**: % of lines executed
307
+ **✅ DO**:
308
+ - Aim for 80-90% coverage
309
+ - Focus on business logic
310
+ - Test edge cases
311
+ - Test error paths
606
312
 
607
- **Coverage Best Practices**:
608
- - Aim for 80%+ overall coverage
609
- - 100% coverage ≠ bug-free (test quality matters)
610
- - Focus on critical paths and edge cases
611
- - Use coverage to find untested code
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
- ### 12. Test Organization
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
- **Naming Conventions**:
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
- ### 13. Test Fixtures & Helpers
649
- **Reusable Test Data and Utilities**
325
+ **Good use cases**:
326
+ - UI component output
327
+ - API responses
328
+ - Configuration objects
329
+ - Error messages
650
330
 
651
331
  ```typescript
652
- // fixtures/user.fixture.ts
653
- export const createUser = (overrides = {}) => ({
654
- id: '1',
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
- export const createUserList = (count = 3) =>
662
- Array.from({ length: count }, (_, i) =>
663
- createUser({ id: String(i + 1), name: `User ${i + 1}` })
664
- );
337
+ // Update snapshots: vitest -u
338
+ ```
665
339
 
666
- // Usage in tests
667
- import { createUser, createUserList } from '../fixtures/user.fixture';
340
+ **Avoid snapshots for**:
341
+ - Dates/timestamps
342
+ - Random values
343
+ - Large objects (prefer specific assertions)
668
344
 
669
- describe('UserRepository', () => {
670
- it('should save user', async () => {
671
- const user = createUser({ name: 'Jane' });
672
- await repo.save(user);
345
+ ---
673
346
 
674
- expect(await repo.findById(user.id)).toEqual(user);
675
- });
347
+ ## Test Organization
676
348
 
677
- it('should find all users', async () => {
678
- const users = createUserList(5);
679
- await Promise.all(users.map(u => repo.save(u)));
349
+ ### File Structure
680
350
 
681
- expect(await repo.findAll()).toHaveLength(5);
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
- **Test Helpers**
365
+ ### Test Naming
687
366
 
367
+ **✅ GOOD**:
688
368
  ```typescript
689
- // helpers/test-utils.ts
690
- export const waitFor = (condition: () => boolean, timeout = 1000) => {
691
- return new Promise((resolve, reject) => {
692
- const startTime = Date.now();
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
- ### 14. Advanced Matchers
716
- **Custom and Built-in Matchers**
717
-
376
+ **❌ BAD**:
718
377
  ```typescript
719
- describe('Advanced Matchers', () => {
720
- // Equality
721
- it('exact equality', () => expect(1 + 1).toBe(2));
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
- **Custom Matchers**
384
+ ---
385
+
386
+ ## Error Handling Tests
782
387
 
783
388
  ```typescript
784
- import { expect } from 'vitest';
785
-
786
- expect.extend({
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
- // Usage
800
- it('should validate email', () => {
801
- expect('user@example.com').toBeValidEmail();
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
- // Run before each test
825
- beforeEach(async () => {
826
- await db.clear();
827
- await db.seed();
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
- it('should delete user', async () => {
842
- await db.insert('users', { name: 'John' });
843
- await db.delete('users', { name: 'John' });
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
- **Conditional Execution**
410
+ ---
851
411
 
852
- ```typescript
853
- // Skip tests
854
- it.skip('not ready yet', () => {});
855
- it.todo('implement later');
412
+ ## Test Isolation
856
413
 
857
- // Only run specific tests
858
- it.only('focus on this test', () => {});
414
+ ### Reset State Between Tests
859
415
 
860
- // Run if condition met
861
- it.runIf(process.env.CI)('CI only test', () => {});
416
+ ```typescript
417
+ let service: UserService;
862
418
 
863
- // Skip if condition met
864
- it.skipIf(process.platform === 'win32')('Unix only test', () => {});
419
+ beforeEach(() => {
420
+ service = new UserService();
421
+ vi.clearAllMocks();
422
+ });
865
423
 
866
- // Concurrent execution
867
- describe.concurrent('parallel tests', () => {
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
- ## Best Practices
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
- **Do**:
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
- class Counter {
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
- it('should start at 0', () => {
930
- expect(counter.getValue()).toBe(0);
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
- ### Testing Utilities
946
- ```typescript
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
- ### Testing Hooks (React, Vue)
444
+ **✅ GOOD**:
959
445
  ```typescript
960
- import { renderHook, waitFor } from '@testing-library/react';
961
- import { useCounter } from './useCounter';
962
-
963
- describe('useCounter', () => {
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
- ## Troubleshooting
453
+ ---
977
454
 
978
- ### Common Issues
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
- ### Debug Strategies
986
- ```bash
987
- # Run single test
988
- npm test -- path/to/test.ts
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
- # Run tests matching pattern
991
- npm test -- --grep "UserService"
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
- # Debug mode (Node inspector)
994
- node --inspect-brk node_modules/.bin/vitest run
478
+ ---
995
479
 
996
- # Watch mode
997
- npm test -- --watch
480
+ ## Quick Reference
998
481
 
999
- # Verbose output
1000
- npm test -- --reporter=verbose
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
- # Coverage report
1003
- npm test -- --coverage
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
- ## Resources
1007
- - **Vitest Docs**: https://vitest.dev
1008
- - **Testing Library**: https://testing-library.com
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.**