metacoding 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +68 -43
  2. package/LICENSE +1 -1
  3. package/lib/services/template-manager.d.ts +3 -0
  4. package/lib/services/template-manager.d.ts.map +1 -1
  5. package/lib/services/template-manager.js +126 -9
  6. package/lib/services/template-manager.js.map +1 -1
  7. package/lib/services/vscode.js +1 -1
  8. package/lib/services/vscode.js.map +1 -1
  9. package/package.json +4 -4
  10. package/templates/general/code-review.instructions.md +265 -0
  11. package/templates/general/{files/copilot-instructions.md.template → copilot-instructions.md} +97 -140
  12. package/templates/{python/files → general}/docs-update.instructions.md +45 -32
  13. package/templates/general/release.instructions.md +242 -0
  14. package/templates/general/test-runner.instructions.md +188 -0
  15. package/templates/node/nodejs.coding.instructions.md +249 -0
  16. package/templates/node/nodejs.docs.instructions.md +234 -0
  17. package/templates/node/nodejs.testing.instructions.md +373 -0
  18. package/templates/python/python.coding.instructions.md +339 -0
  19. package/templates/python/python.docs.instructions.md +1147 -0
  20. package/templates/python/python.testing.instructions.md +1074 -0
  21. package/templates/react/react.coding.instructions.md +695 -0
  22. package/templates/react/react.docs.instructions.md +427 -0
  23. package/templates/react/react.testing.instructions.md +193 -0
  24. package/templates/react/test-runner.instructions.md +135 -0
  25. package/templates/typescript/template.json +16 -0
  26. package/templates/typescript/typescript.coding.instructions.md +368 -0
  27. package/templates/typescript/typescript.docs.instructions.md +734 -0
  28. package/templates/typescript/typescript.testing.instructions.md +740 -0
  29. package/templates/general/files/code-review.instructions.md +0 -111
  30. package/templates/general/files/docs-update.instructions.md +0 -203
  31. package/templates/general/files/release.instructions.md +0 -72
  32. package/templates/general/files/test-runner.instructions.md +0 -107
  33. package/templates/node/files/code-review.instructions.md +0 -222
  34. package/templates/node/files/copilot-instructions.md.template +0 -391
  35. package/templates/node/files/docs-update.instructions.md +0 -203
  36. package/templates/node/files/release.instructions.md +0 -72
  37. package/templates/node/files/test-runner.instructions.md +0 -108
  38. package/templates/python/files/code-review.instructions.md +0 -215
  39. package/templates/python/files/copilot-instructions.md.template +0 -418
  40. package/templates/python/files/release.instructions.md +0 -72
  41. package/templates/python/files/test-runner.instructions.md +0 -108
  42. package/templates/react/files/code-review.instructions.md +0 -160
  43. package/templates/react/files/copilot-instructions.md.template +0 -472
  44. package/templates/react/files/docs-update.instructions.md +0 -203
  45. package/templates/react/files/release.instructions.md +0 -72
  46. package/templates/react/files/test-runner.instructions.md +0 -108
@@ -0,0 +1,740 @@
1
+ ---
2
+ description: 'TypeScript/Node.js-specific testing patterns and frameworks'
3
+ applyTo: 'test/**/*.{ts,js}'
4
+ language: 'typescript'
5
+ ---
6
+
7
+ # TypeScript/Node.js Testing Standards
8
+
9
+ ## Testing Framework and Setup
10
+
11
+ ### Primary Testing Framework: Jest
12
+
13
+ ```typescript
14
+ // jest.config.js
15
+ module.exports = {
16
+ preset: 'ts-jest',
17
+ testEnvironment: 'node',
18
+ roots: ['<rootDir>/src', '<rootDir>/test'],
19
+ testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
20
+ transform: {
21
+ '^.+\\.ts$': 'ts-jest',
22
+ },
23
+ collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
24
+ coverageDirectory: 'coverage',
25
+ coverageReporters: ['text', 'lcov', 'html'],
26
+ };
27
+ ```
28
+
29
+ ### Test Case Naming Conventions
30
+
31
+ **Area Prefixes for Node.js/Backend:**
32
+
33
+ - `API` - API endpoint tests
34
+ - `SRV` - Service layer tests
35
+ - `DB` - Database/Data layer tests
36
+ - `MW` - Middleware tests
37
+ - `ROUTE` - Routing logic tests
38
+ - `AUTH` - Authentication/Authorization tests
39
+ - `UTIL` - Utility function tests
40
+ - `CONFIG` - Configuration management tests
41
+ - `CORE` - Core business logic tests
42
+ - `INT` - Integration tests
43
+ - `E2E` - End-to-end tests
44
+
45
+ **Examples:**
46
+
47
+ - `API-UNIT-001` - First unit test for API endpoints
48
+ - `SRV-UNIT-001` - First unit test for Service layer
49
+ - `DB-INT-001` - First integration test for Database
50
+ - `E2E-FLOW-001` - First end-to-end workflow test
51
+
52
+ ## Unit Testing Patterns
53
+
54
+ ### Service Layer Testing
55
+
56
+ ```typescript
57
+ // test/unit/user-service.test.ts
58
+ import { UserService } from '../../src/services/user-service';
59
+ import { UserRepository } from '../../src/repositories/user-repository';
60
+ import { User } from '../../src/types/user';
61
+
62
+ // Mock the repository
63
+ jest.mock('../../src/repositories/user-repository');
64
+ const MockUserRepository = UserRepository as jest.MockedClass<
65
+ typeof UserRepository
66
+ >;
67
+
68
+ describe('UserService', () => {
69
+ let userService: UserService;
70
+ let mockUserRepository: jest.Mocked<UserRepository>;
71
+
72
+ beforeEach(() => {
73
+ mockUserRepository =
74
+ new MockUserRepository() as jest.Mocked<UserRepository>;
75
+ userService = new UserService(mockUserRepository);
76
+ });
77
+
78
+ afterEach(() => {
79
+ jest.clearAllMocks();
80
+ });
81
+
82
+ describe('getUserById', () => {
83
+ it('should return user when user exists', async () => {
84
+ // Arrange
85
+ const userId = '123';
86
+ const expectedUser: User = {
87
+ id: userId,
88
+ name: 'John Doe',
89
+ email: 'john@example.com',
90
+ };
91
+ mockUserRepository.findById.mockResolvedValue(expectedUser);
92
+
93
+ // Act
94
+ const result = await userService.getUserById(userId);
95
+
96
+ // Assert
97
+ expect(result).toEqual(expectedUser);
98
+ expect(mockUserRepository.findById).toHaveBeenCalledWith(userId);
99
+ expect(mockUserRepository.findById).toHaveBeenCalledTimes(1);
100
+ });
101
+
102
+ it('should throw error when user not found', async () => {
103
+ // Arrange
104
+ const userId = '999';
105
+ mockUserRepository.findById.mockResolvedValue(null);
106
+
107
+ // Act & Assert
108
+ await expect(userService.getUserById(userId)).rejects.toThrow(
109
+ 'User not found'
110
+ );
111
+
112
+ expect(mockUserRepository.findById).toHaveBeenCalledWith(userId);
113
+ });
114
+ });
115
+ });
116
+ ```
117
+
118
+ ### API Endpoint Testing
119
+
120
+ ```typescript
121
+ // test/unit/user-controller.test.ts
122
+ import request from 'supertest';
123
+ import { app } from '../../src/app';
124
+ import { UserService } from '../../src/services/user-service';
125
+
126
+ // Mock the service
127
+ jest.mock('../../src/services/user-service');
128
+ const MockUserService = UserService as jest.MockedClass<typeof UserService>;
129
+
130
+ describe('User Controller', () => {
131
+ let mockUserService: jest.Mocked<UserService>;
132
+
133
+ beforeEach(() => {
134
+ mockUserService = new MockUserService() as jest.Mocked<UserService>;
135
+ // Inject mock into controller if using dependency injection
136
+ });
137
+
138
+ afterEach(() => {
139
+ jest.clearAllMocks();
140
+ });
141
+
142
+ describe('GET /api/users/:id', () => {
143
+ it('should return user when valid id provided', async () => {
144
+ // Arrange
145
+ const userId = '123';
146
+ const mockUser = {
147
+ id: userId,
148
+ name: 'John Doe',
149
+ email: 'john@example.com',
150
+ };
151
+ mockUserService.getUserById.mockResolvedValue(mockUser);
152
+
153
+ // Act
154
+ const response = await request(app)
155
+ .get(`/api/users/${userId}`)
156
+ .expect(200);
157
+
158
+ // Assert
159
+ expect(response.body).toEqual(mockUser);
160
+ expect(mockUserService.getUserById).toHaveBeenCalledWith(userId);
161
+ });
162
+
163
+ it('should return 404 when user not found', async () => {
164
+ // Arrange
165
+ const userId = '999';
166
+ mockUserService.getUserById.mockRejectedValue(
167
+ new Error('User not found')
168
+ );
169
+
170
+ // Act
171
+ const response = await request(app)
172
+ .get(`/api/users/${userId}`)
173
+ .expect(404);
174
+
175
+ // Assert
176
+ expect(response.body).toEqual({ error: 'User not found' });
177
+ });
178
+
179
+ it('should return 400 when invalid id format', async () => {
180
+ // Arrange
181
+ const invalidId = 'invalid-id';
182
+
183
+ // Act
184
+ const response = await request(app)
185
+ .get(`/api/users/${invalidId}`)
186
+ .expect(400);
187
+
188
+ // Assert
189
+ expect(response.body).toEqual({ error: 'Invalid user ID format' });
190
+ });
191
+ });
192
+ });
193
+ ```
194
+
195
+ ### Utility Function Testing
196
+
197
+ ```typescript
198
+ // test/unit/string-utils.test.ts
199
+ import { capitalize, slugify, truncate } from '../../src/utils/string-utils';
200
+
201
+ describe('String Utils', () => {
202
+ describe('capitalize', () => {
203
+ it('should capitalize first letter of string', () => {
204
+ expect(capitalize('hello')).toBe('Hello');
205
+ });
206
+
207
+ it('should handle empty string', () => {
208
+ expect(capitalize('')).toBe('');
209
+ });
210
+
211
+ it('should handle single character', () => {
212
+ expect(capitalize('a')).toBe('A');
213
+ });
214
+
215
+ it('should not change already capitalized string', () => {
216
+ expect(capitalize('Hello')).toBe('Hello');
217
+ });
218
+ });
219
+
220
+ describe('slugify', () => {
221
+ it('should convert spaces to hyphens', () => {
222
+ expect(slugify('Hello World')).toBe('hello-world');
223
+ });
224
+
225
+ it('should remove special characters', () => {
226
+ expect(slugify('Hello! @World#')).toBe('hello-world');
227
+ });
228
+
229
+ it('should handle multiple spaces', () => {
230
+ expect(slugify('Hello World')).toBe('hello-world');
231
+ });
232
+ });
233
+
234
+ describe('truncate', () => {
235
+ it('should truncate long strings', () => {
236
+ const longString = 'This is a very long string that should be truncated';
237
+ expect(truncate(longString, 20)).toBe('This is a very lo...');
238
+ });
239
+
240
+ it('should not truncate short strings', () => {
241
+ const shortString = 'Short';
242
+ expect(truncate(shortString, 20)).toBe('Short');
243
+ });
244
+ });
245
+ });
246
+ ```
247
+
248
+ ## Integration Testing
249
+
250
+ ### Database Integration Testing
251
+
252
+ ```typescript
253
+ // test/integration/user-repository.test.ts
254
+ import { UserRepository } from '../../src/repositories/user-repository';
255
+ import { DatabaseConnection } from '../../src/database/connection';
256
+ import { User } from '../../src/types/user';
257
+
258
+ describe('UserRepository Integration', () => {
259
+ let userRepository: UserRepository;
260
+ let db: DatabaseConnection;
261
+
262
+ beforeAll(async () => {
263
+ // Setup test database
264
+ db = new DatabaseConnection(process.env.TEST_DATABASE_URL);
265
+ await db.connect();
266
+ userRepository = new UserRepository(db);
267
+ });
268
+
269
+ afterAll(async () => {
270
+ await db.disconnect();
271
+ });
272
+
273
+ beforeEach(async () => {
274
+ // Clean database before each test
275
+ await db.query('DELETE FROM users');
276
+ });
277
+
278
+ describe('findById', () => {
279
+ it('should find existing user', async () => {
280
+ // Arrange
281
+ const userData: Omit<User, 'id'> = {
282
+ name: 'John Doe',
283
+ email: 'john@example.com',
284
+ };
285
+ const createdUser = await userRepository.create(userData);
286
+
287
+ // Act
288
+ const foundUser = await userRepository.findById(createdUser.id);
289
+
290
+ // Assert
291
+ expect(foundUser).toEqual(createdUser);
292
+ });
293
+
294
+ it('should return null for non-existent user', async () => {
295
+ // Act
296
+ const foundUser = await userRepository.findById('999');
297
+
298
+ // Assert
299
+ expect(foundUser).toBeNull();
300
+ });
301
+ });
302
+
303
+ describe('create', () => {
304
+ it('should create new user with generated id', async () => {
305
+ // Arrange
306
+ const userData: Omit<User, 'id'> = {
307
+ name: 'Jane Doe',
308
+ email: 'jane@example.com',
309
+ };
310
+
311
+ // Act
312
+ const createdUser = await userRepository.create(userData);
313
+
314
+ // Assert
315
+ expect(createdUser).toMatchObject(userData);
316
+ expect(createdUser.id).toBeDefined();
317
+ expect(typeof createdUser.id).toBe('string');
318
+ });
319
+
320
+ it('should enforce unique email constraint', async () => {
321
+ // Arrange
322
+ const userData: Omit<User, 'id'> = {
323
+ name: 'John Doe',
324
+ email: 'duplicate@example.com',
325
+ };
326
+ await userRepository.create(userData);
327
+
328
+ // Act & Assert
329
+ await expect(userRepository.create(userData)).rejects.toThrow(
330
+ 'Email already exists'
331
+ );
332
+ });
333
+ });
334
+ });
335
+ ```
336
+
337
+ ### API Integration Testing
338
+
339
+ ```typescript
340
+ // test/integration/user-api.test.ts
341
+ import request from 'supertest';
342
+ import { app } from '../../src/app';
343
+ import { DatabaseConnection } from '../../src/database/connection';
344
+
345
+ describe('User API Integration', () => {
346
+ let db: DatabaseConnection;
347
+
348
+ beforeAll(async () => {
349
+ db = new DatabaseConnection(process.env.TEST_DATABASE_URL);
350
+ await db.connect();
351
+ });
352
+
353
+ afterAll(async () => {
354
+ await db.disconnect();
355
+ });
356
+
357
+ beforeEach(async () => {
358
+ await db.query('DELETE FROM users');
359
+ });
360
+
361
+ describe('POST /api/users', () => {
362
+ it('should create new user and return 201', async () => {
363
+ // Arrange
364
+ const userData = {
365
+ name: 'John Doe',
366
+ email: 'john@example.com',
367
+ };
368
+
369
+ // Act
370
+ const response = await request(app)
371
+ .post('/api/users')
372
+ .send(userData)
373
+ .expect(201);
374
+
375
+ // Assert
376
+ expect(response.body).toMatchObject(userData);
377
+ expect(response.body.id).toBeDefined();
378
+ });
379
+
380
+ it('should validate required fields', async () => {
381
+ // Arrange
382
+ const invalidData = {
383
+ name: 'John Doe',
384
+ // Missing email
385
+ };
386
+
387
+ // Act
388
+ const response = await request(app)
389
+ .post('/api/users')
390
+ .send(invalidData)
391
+ .expect(400);
392
+
393
+ // Assert
394
+ expect(response.body.error).toContain('email is required');
395
+ });
396
+ });
397
+
398
+ describe('GET /api/users', () => {
399
+ it('should return list of users', async () => {
400
+ // Arrange
401
+ const users = [
402
+ { name: 'John Doe', email: 'john@example.com' },
403
+ { name: 'Jane Doe', email: 'jane@example.com' },
404
+ ];
405
+
406
+ for (const user of users) {
407
+ await request(app).post('/api/users').send(user);
408
+ }
409
+
410
+ // Act
411
+ const response = await request(app).get('/api/users').expect(200);
412
+
413
+ // Assert
414
+ expect(response.body).toHaveLength(2);
415
+ expect(response.body[0]).toMatchObject(users[0]);
416
+ expect(response.body[1]).toMatchObject(users[1]);
417
+ });
418
+ });
419
+ });
420
+ ```
421
+
422
+ ## Mocking Strategies
423
+
424
+ ### External Service Mocking
425
+
426
+ ```typescript
427
+ // test/mocks/external-api.ts
428
+ import nock from 'nock';
429
+
430
+ export const mockExternalAPI = {
431
+ mockUserLookup: (userId: string, userData: any) => {
432
+ return nock('https://api.external-service.com')
433
+ .get(`/users/${userId}`)
434
+ .reply(200, userData);
435
+ },
436
+
437
+ mockUserLookupError: (userId: string, statusCode: number = 500) => {
438
+ return nock('https://api.external-service.com')
439
+ .get(`/users/${userId}`)
440
+ .reply(statusCode, { error: 'External service error' });
441
+ },
442
+
443
+ cleanup: () => {
444
+ nock.cleanAll();
445
+ },
446
+ };
447
+
448
+ // Usage in tests
449
+ describe('External Service Integration', () => {
450
+ afterEach(() => {
451
+ mockExternalAPI.cleanup();
452
+ });
453
+
454
+ it('should handle external API success', async () => {
455
+ // Arrange
456
+ const userId = '123';
457
+ const userData = { id: userId, name: 'John Doe' };
458
+ mockExternalAPI.mockUserLookup(userId, userData);
459
+
460
+ // Act & Assert
461
+ const result = await externalService.getUser(userId);
462
+ expect(result).toEqual(userData);
463
+ });
464
+ });
465
+ ```
466
+
467
+ ### File System Mocking
468
+
469
+ ```typescript
470
+ // test/unit/file-service.test.ts
471
+ import fs from 'fs/promises';
472
+ import { FileService } from '../../src/services/file-service';
473
+
474
+ // Mock fs module
475
+ jest.mock('fs/promises');
476
+ const mockFs = fs as jest.Mocked<typeof fs>;
477
+
478
+ describe('FileService', () => {
479
+ let fileService: FileService;
480
+
481
+ beforeEach(() => {
482
+ fileService = new FileService();
483
+ });
484
+
485
+ afterEach(() => {
486
+ jest.clearAllMocks();
487
+ });
488
+
489
+ describe('readConfig', () => {
490
+ it('should read and parse JSON config file', async () => {
491
+ // Arrange
492
+ const configData = { host: 'localhost', port: 3000 };
493
+ const configJson = JSON.stringify(configData);
494
+ mockFs.readFile.mockResolvedValue(configJson);
495
+
496
+ // Act
497
+ const result = await fileService.readConfig('/path/to/config.json');
498
+
499
+ // Assert
500
+ expect(result).toEqual(configData);
501
+ expect(mockFs.readFile).toHaveBeenCalledWith(
502
+ '/path/to/config.json',
503
+ 'utf8'
504
+ );
505
+ });
506
+
507
+ it('should handle file read errors', async () => {
508
+ // Arrange
509
+ mockFs.readFile.mockRejectedValue(new Error('File not found'));
510
+
511
+ // Act & Assert
512
+ await expect(
513
+ fileService.readConfig('/nonexistent/config.json')
514
+ ).rejects.toThrow('File not found');
515
+ });
516
+ });
517
+ });
518
+ ```
519
+
520
+ ## Async Testing Patterns
521
+
522
+ ### Promise Testing
523
+
524
+ ```typescript
525
+ // test/unit/async-service.test.ts
526
+ import { AsyncService } from '../../src/services/async-service';
527
+
528
+ describe('AsyncService', () => {
529
+ let asyncService: AsyncService;
530
+
531
+ beforeEach(() => {
532
+ asyncService = new AsyncService();
533
+ });
534
+
535
+ describe('processData', () => {
536
+ it('should process data asynchronously', async () => {
537
+ // Arrange
538
+ const inputData = ['item1', 'item2', 'item3'];
539
+
540
+ // Act
541
+ const result = await asyncService.processData(inputData);
542
+
543
+ // Assert
544
+ expect(result).toHaveLength(3);
545
+ expect(result[0]).toContain('processed');
546
+ });
547
+
548
+ it('should handle processing errors', async () => {
549
+ // Arrange
550
+ const invalidData = [null, undefined];
551
+
552
+ // Act & Assert
553
+ await expect(asyncService.processData(invalidData)).rejects.toThrow(
554
+ 'Invalid data provided'
555
+ );
556
+ });
557
+ });
558
+
559
+ describe('processWithTimeout', () => {
560
+ it('should complete within timeout', async () => {
561
+ // Arrange
562
+ const data = 'test data';
563
+ const timeout = 1000;
564
+
565
+ // Act
566
+ const startTime = Date.now();
567
+ const result = await asyncService.processWithTimeout(data, timeout);
568
+ const endTime = Date.now();
569
+
570
+ // Assert
571
+ expect(result).toBeDefined();
572
+ expect(endTime - startTime).toBeLessThan(timeout);
573
+ });
574
+
575
+ it('should timeout for long operations', async () => {
576
+ // Arrange
577
+ const data = 'long processing data';
578
+ const shortTimeout = 100;
579
+
580
+ // Act & Assert
581
+ await expect(
582
+ asyncService.processWithTimeout(data, shortTimeout)
583
+ ).rejects.toThrow('Operation timed out');
584
+ });
585
+ });
586
+ });
587
+ ```
588
+
589
+ ### Event Emitter Testing
590
+
591
+ ```typescript
592
+ // test/unit/event-service.test.ts
593
+ import { EventService } from '../../src/services/event-service';
594
+
595
+ describe('EventService', () => {
596
+ let eventService: EventService;
597
+
598
+ beforeEach(() => {
599
+ eventService = new EventService();
600
+ });
601
+
602
+ afterEach(() => {
603
+ eventService.removeAllListeners();
604
+ });
605
+
606
+ describe('user events', () => {
607
+ it('should emit user created event', (done) => {
608
+ // Arrange
609
+ const userData = { id: '123', name: 'John Doe' };
610
+
611
+ // Act
612
+ eventService.on('user:created', (data) => {
613
+ // Assert
614
+ expect(data).toEqual(userData);
615
+ done();
616
+ });
617
+
618
+ eventService.createUser(userData);
619
+ });
620
+
621
+ it('should handle multiple listeners', () => {
622
+ // Arrange
623
+ const userData = { id: '123', name: 'John Doe' };
624
+ const listener1 = jest.fn();
625
+ const listener2 = jest.fn();
626
+
627
+ eventService.on('user:created', listener1);
628
+ eventService.on('user:created', listener2);
629
+
630
+ // Act
631
+ eventService.createUser(userData);
632
+
633
+ // Assert
634
+ expect(listener1).toHaveBeenCalledWith(userData);
635
+ expect(listener2).toHaveBeenCalledWith(userData);
636
+ });
637
+ });
638
+ });
639
+ ```
640
+
641
+ ## Performance Testing
642
+
643
+ ### Benchmark Testing
644
+
645
+ ```typescript
646
+ // test/performance/string-utils.perf.test.ts
647
+ import { performance } from 'perf_hooks';
648
+ import { processLargeString } from '../../src/utils/string-utils';
649
+
650
+ describe('String Utils Performance', () => {
651
+ const PERFORMANCE_THRESHOLD = 100; // milliseconds
652
+
653
+ describe('processLargeString', () => {
654
+ it('should process large string within performance threshold', () => {
655
+ // Arrange
656
+ const largeString = 'x'.repeat(1000000); // 1MB string
657
+
658
+ // Act
659
+ const startTime = performance.now();
660
+ const result = processLargeString(largeString);
661
+ const endTime = performance.now();
662
+
663
+ // Assert
664
+ expect(result).toBeDefined();
665
+ expect(endTime - startTime).toBeLessThan(PERFORMANCE_THRESHOLD);
666
+ });
667
+ });
668
+ });
669
+ ```
670
+
671
+ ## Test Utilities and Helpers
672
+
673
+ ### Test Data Factories
674
+
675
+ ```typescript
676
+ // test/factories/user-factory.ts
677
+ import { User } from '../../src/types/user';
678
+
679
+ export class UserFactory {
680
+ static create(overrides: Partial<User> = {}): User {
681
+ return {
682
+ id: Math.random().toString(36).substr(2, 9),
683
+ name: 'Test User',
684
+ email: 'test@example.com',
685
+ createdAt: new Date(),
686
+ ...overrides,
687
+ };
688
+ }
689
+
690
+ static createMany(count: number, overrides: Partial<User> = {}): User[] {
691
+ return Array.from({ length: count }, (_, index) =>
692
+ this.create({
693
+ ...overrides,
694
+ email: `test${index}@example.com`,
695
+ })
696
+ );
697
+ }
698
+ }
699
+
700
+ // Usage in tests
701
+ describe('User Service', () => {
702
+ it('should process multiple users', () => {
703
+ const users = UserFactory.createMany(5);
704
+ expect(users).toHaveLength(5);
705
+ expect(users[0].email).toBe('test0@example.com');
706
+ });
707
+ });
708
+ ```
709
+
710
+ ### Custom Matchers
711
+
712
+ ```typescript
713
+ // test/matchers/custom-matchers.ts
714
+ export const customMatchers = {
715
+ toBeValidUser(received: any) {
716
+ const pass =
717
+ typeof received === 'object' &&
718
+ typeof received.id === 'string' &&
719
+ typeof received.name === 'string' &&
720
+ typeof received.email === 'string' &&
721
+ received.email.includes('@');
722
+
723
+ return {
724
+ message: () => `Expected ${received} to be a valid user object`,
725
+ pass,
726
+ };
727
+ },
728
+ };
729
+
730
+ // Setup in test files
731
+ expect.extend(customMatchers);
732
+
733
+ // Usage
734
+ describe('User Service', () => {
735
+ it('should return valid user', () => {
736
+ const user = userService.getUser('123');
737
+ expect(user).toBeValidUser();
738
+ });
739
+ });
740
+ ```