agentic-team-templates 0.4.2 → 0.6.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.
@@ -0,0 +1,565 @@
1
+ # Test Data Management
2
+
3
+ Guidelines for managing test data effectively.
4
+
5
+ ## Core Principles
6
+
7
+ 1. **Realistic Data** - Test data should resemble production data
8
+ 2. **Isolated State** - Each test owns its data
9
+ 3. **Minimal Setup** - Only create what the test needs
10
+ 4. **Deterministic** - Same test always uses same logical data
11
+ 5. **No Production Data** - Never use real user data
12
+
13
+ ## Faker for Realistic Data
14
+
15
+ Use Faker to generate realistic test data:
16
+
17
+ ```ts
18
+ // factories/index.ts
19
+ import { faker } from '@faker-js/faker';
20
+
21
+ // Seed for deterministic tests (optional)
22
+ faker.seed(12345);
23
+
24
+ export const factories = {
25
+ user: (overrides: Partial<User> = {}): User => ({
26
+ id: faker.string.uuid(),
27
+ email: faker.internet.email(),
28
+ name: faker.person.fullName(),
29
+ phone: faker.phone.number(),
30
+ address: {
31
+ street: faker.location.streetAddress(),
32
+ city: faker.location.city(),
33
+ state: faker.location.state(),
34
+ zip: faker.location.zipCode(),
35
+ },
36
+ createdAt: faker.date.past(),
37
+ updatedAt: new Date(),
38
+ ...overrides,
39
+ }),
40
+
41
+ product: (overrides: Partial<Product> = {}): Product => ({
42
+ id: faker.string.uuid(),
43
+ name: faker.commerce.productName(),
44
+ description: faker.commerce.productDescription(),
45
+ price: parseFloat(faker.commerce.price({ min: 10, max: 1000 })),
46
+ sku: faker.string.alphanumeric(10).toUpperCase(),
47
+ inventory: faker.number.int({ min: 0, max: 100 }),
48
+ category: faker.commerce.department(),
49
+ createdAt: faker.date.past(),
50
+ ...overrides,
51
+ }),
52
+
53
+ order: (overrides: Partial<Order> = {}): Order => ({
54
+ id: faker.string.uuid(),
55
+ userId: faker.string.uuid(),
56
+ items: [],
57
+ total: 0,
58
+ status: 'pending',
59
+ shippingAddress: factories.address(),
60
+ createdAt: new Date(),
61
+ updatedAt: new Date(),
62
+ ...overrides,
63
+ }),
64
+
65
+ address: (overrides: Partial<Address> = {}): Address => ({
66
+ street: faker.location.streetAddress(),
67
+ city: faker.location.city(),
68
+ state: faker.location.state(),
69
+ zip: faker.location.zipCode(),
70
+ country: 'US',
71
+ ...overrides,
72
+ }),
73
+ };
74
+ ```
75
+
76
+ ## Database Factories
77
+
78
+ For integration tests that need real database records:
79
+
80
+ ```ts
81
+ // factories/db.ts
82
+ import { db } from '../lib/db';
83
+ import { factories } from './index';
84
+
85
+ export const dbFactories = {
86
+ user: async (overrides: Partial<User> = {}): Promise<User> => {
87
+ const userData = factories.user(overrides);
88
+ return db.user.create({ data: userData });
89
+ },
90
+
91
+ product: async (overrides: Partial<Product> = {}): Promise<Product> => {
92
+ const productData = factories.product(overrides);
93
+ return db.product.create({ data: productData });
94
+ },
95
+
96
+ order: async (
97
+ user: User,
98
+ items: Array<{ product: Product; quantity: number }>
99
+ ): Promise<Order> => {
100
+ const total = items.reduce(
101
+ (sum, item) => sum + item.product.price * item.quantity,
102
+ 0
103
+ );
104
+
105
+ return db.order.create({
106
+ data: {
107
+ userId: user.id,
108
+ total,
109
+ status: 'pending',
110
+ items: {
111
+ create: items.map((item) => ({
112
+ productId: item.product.id,
113
+ quantity: item.quantity,
114
+ price: item.product.price,
115
+ })),
116
+ },
117
+ },
118
+ include: { items: true },
119
+ });
120
+ },
121
+ };
122
+ ```
123
+
124
+ ## Builder Pattern for Complex Objects
125
+
126
+ When objects have many configurations:
127
+
128
+ ```ts
129
+ // builders/OrderBuilder.ts
130
+ import { faker } from '@faker-js/faker';
131
+
132
+ export class OrderBuilder {
133
+ private data: Partial<Order> = {};
134
+ private items: OrderItem[] = [];
135
+
136
+ static create(): OrderBuilder {
137
+ return new OrderBuilder();
138
+ }
139
+
140
+ withUser(user: User): this {
141
+ this.data.userId = user.id;
142
+ return this;
143
+ }
144
+
145
+ withItem(product: Product, quantity: number = 1): this {
146
+ this.items.push({
147
+ id: faker.string.uuid(),
148
+ productId: product.id,
149
+ quantity,
150
+ price: product.price,
151
+ });
152
+ return this;
153
+ }
154
+
155
+ withStatus(status: OrderStatus): this {
156
+ this.data.status = status;
157
+ return this;
158
+ }
159
+
160
+ withDiscount(discount: Discount): this {
161
+ this.data.discount = discount;
162
+ return this;
163
+ }
164
+
165
+ shipped(): this {
166
+ this.data.status = 'shipped';
167
+ this.data.shippedAt = new Date();
168
+ return this;
169
+ }
170
+
171
+ cancelled(): this {
172
+ this.data.status = 'cancelled';
173
+ this.data.cancelledAt = new Date();
174
+ return this;
175
+ }
176
+
177
+ build(): Order {
178
+ const total = this.items.reduce(
179
+ (sum, item) => sum + item.price * item.quantity,
180
+ 0
181
+ );
182
+
183
+ return {
184
+ id: faker.string.uuid(),
185
+ userId: this.data.userId ?? faker.string.uuid(),
186
+ items: this.items,
187
+ total: this.data.discount
188
+ ? applyDiscount(total, this.data.discount)
189
+ : total,
190
+ status: this.data.status ?? 'pending',
191
+ discount: this.data.discount,
192
+ shippedAt: this.data.shippedAt,
193
+ cancelledAt: this.data.cancelledAt,
194
+ createdAt: new Date(),
195
+ updatedAt: new Date(),
196
+ };
197
+ }
198
+
199
+ async persist(db: Database): Promise<Order> {
200
+ const order = this.build();
201
+ return db.order.create({
202
+ data: order,
203
+ include: { items: true },
204
+ });
205
+ }
206
+ }
207
+
208
+ // Usage
209
+ const order = OrderBuilder.create()
210
+ .withUser(testUser)
211
+ .withItem(product1, 2)
212
+ .withItem(product2, 1)
213
+ .withDiscount({ type: 'percentage', value: 10 })
214
+ .shipped()
215
+ .build();
216
+ ```
217
+
218
+ ## Object Mother Pattern
219
+
220
+ Pre-configured scenarios for common test cases:
221
+
222
+ ```ts
223
+ // mothers/UserMother.ts
224
+ import { dbFactories } from '../factories/db';
225
+
226
+ export const UserMother = {
227
+ // Simple users
228
+ admin: () => dbFactories.user({ role: 'admin', verified: true }),
229
+ regular: () => dbFactories.user({ role: 'user', verified: true }),
230
+ unverified: () => dbFactories.user({ verified: false }),
231
+ suspended: () => dbFactories.user({ status: 'suspended' }),
232
+
233
+ // Complex scenarios
234
+ async withOrders(count: number = 3): Promise<User> {
235
+ const user = await dbFactories.user();
236
+ const products = await Promise.all(
237
+ Array.from({ length: count }, () => dbFactories.product())
238
+ );
239
+
240
+ for (let i = 0; i < count; i++) {
241
+ await dbFactories.order(user, [{ product: products[i], quantity: 1 }]);
242
+ }
243
+
244
+ return user;
245
+ },
246
+
247
+ async withExpiredSubscription(): Promise<User> {
248
+ const user = await dbFactories.user();
249
+ await db.subscription.create({
250
+ data: {
251
+ userId: user.id,
252
+ plan: 'premium',
253
+ expiresAt: new Date(Date.now() - 86400000), // Yesterday
254
+ },
255
+ });
256
+ return user;
257
+ },
258
+
259
+ async withActiveSubscription(plan: Plan = 'premium'): Promise<User> {
260
+ const user = await dbFactories.user();
261
+ await db.subscription.create({
262
+ data: {
263
+ userId: user.id,
264
+ plan,
265
+ expiresAt: new Date(Date.now() + 30 * 86400000), // 30 days
266
+ },
267
+ });
268
+ return user;
269
+ },
270
+ };
271
+
272
+ // Usage in tests
273
+ it('shows renewal prompt for expired subscription', async () => {
274
+ const user = await UserMother.withExpiredSubscription();
275
+ const result = await checkSubscriptionStatus(user.id);
276
+ expect(result.showRenewalPrompt).toBe(true);
277
+ });
278
+ ```
279
+
280
+ ## Fixtures for Static Data
281
+
282
+ Use fixtures for data that doesn't change:
283
+
284
+ ```ts
285
+ // fixtures/countries.json
286
+ [
287
+ { "code": "US", "name": "United States", "currency": "USD" },
288
+ { "code": "GB", "name": "United Kingdom", "currency": "GBP" },
289
+ { "code": "EU", "name": "European Union", "currency": "EUR" }
290
+ ]
291
+
292
+ // fixtures/taxRates.json
293
+ {
294
+ "US": { "standard": 0.08, "reduced": 0.04, "exempt": 0 },
295
+ "GB": { "standard": 0.20, "reduced": 0.05, "exempt": 0 },
296
+ "EU": { "standard": 0.21, "reduced": 0.10, "exempt": 0 }
297
+ }
298
+
299
+ // Usage
300
+ import countries from '../fixtures/countries.json';
301
+ import taxRates from '../fixtures/taxRates.json';
302
+
303
+ it.each(countries)('calculates tax for $name', ({ code }) => {
304
+ const rate = taxRates[code].standard;
305
+ expect(calculateTax(100, code)).toBe(100 * (1 + rate));
306
+ });
307
+ ```
308
+
309
+ ## Database Cleanup Strategies
310
+
311
+ ### Transaction Rollback (Fastest)
312
+
313
+ ```ts
314
+ // test/setup.ts
315
+ import { db } from '../lib/db';
316
+
317
+ beforeEach(async () => {
318
+ // Start transaction
319
+ await db.$executeRaw`BEGIN`;
320
+ });
321
+
322
+ afterEach(async () => {
323
+ // Rollback - no cleanup needed
324
+ await db.$executeRaw`ROLLBACK`;
325
+ });
326
+ ```
327
+
328
+ ### Truncate Tables
329
+
330
+ ```ts
331
+ // test/setup.ts
332
+ export async function cleanDatabase() {
333
+ const tables = ['order_item', 'order', 'product', 'user'];
334
+
335
+ await db.$transaction(
336
+ tables.map((table) =>
337
+ db.$executeRawUnsafe(`TRUNCATE TABLE "${table}" CASCADE`)
338
+ )
339
+ );
340
+ }
341
+
342
+ beforeEach(async () => {
343
+ await cleanDatabase();
344
+ });
345
+ ```
346
+
347
+ ### Delete in Order (Safest)
348
+
349
+ ```ts
350
+ // test/setup.ts
351
+ export async function cleanDatabase() {
352
+ // Delete in dependency order (children first)
353
+ await db.$transaction([
354
+ db.orderItem.deleteMany(),
355
+ db.order.deleteMany(),
356
+ db.product.deleteMany(),
357
+ db.user.deleteMany(),
358
+ ]);
359
+ }
360
+ ```
361
+
362
+ ## Seeding Test Database
363
+
364
+ ```ts
365
+ // test/seed.ts
366
+ import { dbFactories } from './factories/db';
367
+
368
+ export async function seedTestDatabase() {
369
+ // Create base data
370
+ const admin = await dbFactories.user({
371
+ email: 'admin@test.com',
372
+ role: 'admin',
373
+ });
374
+
375
+ const regularUser = await dbFactories.user({
376
+ email: 'user@test.com',
377
+ role: 'user',
378
+ });
379
+
380
+ const products = await Promise.all([
381
+ dbFactories.product({ name: 'Widget A', price: 100 }),
382
+ dbFactories.product({ name: 'Widget B', price: 200 }),
383
+ dbFactories.product({ name: 'Widget C', price: 50 }),
384
+ ]);
385
+
386
+ return { admin, regularUser, products };
387
+ }
388
+
389
+ // Usage in global setup
390
+ // test/globalSetup.ts
391
+ export default async function globalSetup() {
392
+ const testDb = await createTestDatabase();
393
+ await runMigrations(testDb);
394
+ await seedTestDatabase();
395
+ }
396
+ ```
397
+
398
+ ## Deterministic Random Data
399
+
400
+ When you need "random" data but reproducible tests:
401
+
402
+ ```ts
403
+ // Seed Faker for determinism
404
+ import { faker } from '@faker-js/faker';
405
+
406
+ // Global seed for all tests
407
+ faker.seed(12345);
408
+
409
+ // Or per-test seeding based on test name
410
+ beforeEach((context) => {
411
+ const seed = hashCode(context.task.name);
412
+ faker.seed(seed);
413
+ });
414
+
415
+ function hashCode(str: string): number {
416
+ let hash = 0;
417
+ for (let i = 0; i < str.length; i++) {
418
+ hash = (hash << 5) - hash + str.charCodeAt(i);
419
+ hash |= 0;
420
+ }
421
+ return Math.abs(hash);
422
+ }
423
+ ```
424
+
425
+ ## Snapshot Testing for Data
426
+
427
+ When test data is complex and stability is important:
428
+
429
+ ```ts
430
+ // Capture expected data shape
431
+ it('generates correct order data', () => {
432
+ const order = OrderBuilder.create()
433
+ .withItem(product, 2)
434
+ .build();
435
+
436
+ expect(order).toMatchSnapshot({
437
+ id: expect.any(String),
438
+ createdAt: expect.any(Date),
439
+ updatedAt: expect.any(Date),
440
+ });
441
+ });
442
+ ```
443
+
444
+ ## Parameterized Tests
445
+
446
+ Test multiple scenarios efficiently:
447
+
448
+ ```ts
449
+ // Using test.each
450
+ describe('validateEmail', () => {
451
+ const validEmails = [
452
+ 'test@example.com',
453
+ 'user.name@domain.org',
454
+ 'user+tag@example.co.uk',
455
+ ];
456
+
457
+ const invalidEmails = [
458
+ '',
459
+ 'invalid',
460
+ '@nodomain.com',
461
+ 'spaces in@email.com',
462
+ 'no@tld',
463
+ ];
464
+
465
+ it.each(validEmails)('accepts valid email: %s', (email) => {
466
+ expect(validateEmail(email)).toBe(true);
467
+ });
468
+
469
+ it.each(invalidEmails)('rejects invalid email: %s', (email) => {
470
+ expect(validateEmail(email)).toBe(false);
471
+ });
472
+ });
473
+
474
+ // With test case objects
475
+ describe('calculateShipping', () => {
476
+ const testCases = [
477
+ { weight: 0, distance: 100, expected: 0 },
478
+ { weight: 1, distance: 100, expected: 5 },
479
+ { weight: 5, distance: 100, expected: 15 },
480
+ { weight: 1, distance: 500, expected: 10 },
481
+ { weight: 10, distance: 1000, expected: 50 },
482
+ ];
483
+
484
+ it.each(testCases)(
485
+ 'calculates $expected for weight=$weight, distance=$distance',
486
+ ({ weight, distance, expected }) => {
487
+ expect(calculateShipping(weight, distance)).toBe(expected);
488
+ }
489
+ );
490
+ });
491
+ ```
492
+
493
+ ## Anti-Patterns
494
+
495
+ ### Shared Mutable State
496
+
497
+ ```ts
498
+ // Bad: Tests affect each other
499
+ let testUser = createUser();
500
+
501
+ it('test 1', () => {
502
+ testUser.name = 'Changed';
503
+ });
504
+
505
+ it('test 2', () => {
506
+ expect(testUser.name).toBe('Original'); // Fails!
507
+ });
508
+
509
+ // Good: Fresh data per test
510
+ let testUser: User;
511
+
512
+ beforeEach(() => {
513
+ testUser = createUser();
514
+ });
515
+ ```
516
+
517
+ ### Hard-Coded IDs
518
+
519
+ ```ts
520
+ // Bad: Magic IDs
521
+ const order = await getOrder('550e8400-e29b-41d4-a716-446655440000');
522
+
523
+ // Good: Generated IDs
524
+ const created = await createOrder(data);
525
+ const order = await getOrder(created.id);
526
+ ```
527
+
528
+ ### Production Data
529
+
530
+ ```ts
531
+ // Bad: Using real user data
532
+ const user = { email: 'real.customer@example.com', ... };
533
+
534
+ // Good: Fake data
535
+ const user = factories.user();
536
+ ```
537
+
538
+ ### Over-specified Data
539
+
540
+ ```ts
541
+ // Bad: Too much irrelevant data
542
+ it('calculates discount', () => {
543
+ const order = {
544
+ id: '123',
545
+ userId: 'user-1',
546
+ items: [/* ... */],
547
+ createdAt: new Date(),
548
+ updatedAt: new Date(),
549
+ shippingAddress: { /* ... */ },
550
+ billingAddress: { /* ... */ },
551
+ notes: 'Please handle with care',
552
+ // ... 20 more fields
553
+ };
554
+ expect(calculateDiscount(order)).toBe(10);
555
+ });
556
+
557
+ // Good: Only relevant data
558
+ it('calculates discount', () => {
559
+ const order = factories.order({
560
+ total: 100,
561
+ discount: { type: 'percentage', value: 10 },
562
+ });
563
+ expect(calculateDiscount(order)).toBe(10);
564
+ });
565
+ ```