agentic-team-templates 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,511 @@
1
+ # Test Design Patterns
2
+
3
+ Guidelines for designing effective, maintainable tests.
4
+
5
+ ## Core Patterns
6
+
7
+ ### Arrange-Act-Assert (AAA)
8
+
9
+ The fundamental pattern for all tests:
10
+
11
+ ```ts
12
+ it('calculates order total with discount', () => {
13
+ // Arrange - Set up test data and dependencies
14
+ const items = [
15
+ { price: 100, quantity: 2 },
16
+ { price: 50, quantity: 1 },
17
+ ];
18
+ const discount = { type: 'percentage', value: 10 };
19
+
20
+ // Act - Execute the code under test
21
+ const total = calculateOrderTotal(items, discount);
22
+
23
+ // Assert - Verify the result
24
+ expect(total).toBe(225); // (200 + 50) * 0.9
25
+ });
26
+ ```
27
+
28
+ **Rules:**
29
+ - Separate sections with blank lines
30
+ - One "Act" per test
31
+ - Keep "Arrange" minimal (use factories)
32
+ - Assertions should be obvious
33
+
34
+ ### Given-When-Then (BDD)
35
+
36
+ Express tests in business language:
37
+
38
+ ```ts
39
+ describe('Shopping Cart', () => {
40
+ describe('given a cart with items', () => {
41
+ const cart = createCart([item1, item2]);
42
+
43
+ describe('when applying a discount code', () => {
44
+ const result = cart.applyDiscount('SAVE20');
45
+
46
+ it('then reduces the total by 20%', () => {
47
+ expect(result.total).toBe(originalTotal * 0.8);
48
+ });
49
+
50
+ it('then marks discount as applied', () => {
51
+ expect(result.discountApplied).toBe(true);
52
+ });
53
+ });
54
+ });
55
+ });
56
+ ```
57
+
58
+ ### Test Isolation
59
+
60
+ Each test must be completely independent:
61
+
62
+ ```ts
63
+ // Bad: Shared mutable state
64
+ let counter = 0;
65
+
66
+ it('test 1', () => {
67
+ counter++;
68
+ expect(counter).toBe(1);
69
+ });
70
+
71
+ it('test 2', () => {
72
+ expect(counter).toBe(0); // FAILS! Depends on test order
73
+ });
74
+
75
+ // Good: Fresh state per test
76
+ describe('Counter', () => {
77
+ let counter: Counter;
78
+
79
+ beforeEach(() => {
80
+ counter = new Counter();
81
+ });
82
+
83
+ it('starts at zero', () => {
84
+ expect(counter.value).toBe(0);
85
+ });
86
+
87
+ it('increments by one', () => {
88
+ counter.increment();
89
+ expect(counter.value).toBe(1);
90
+ });
91
+ });
92
+ ```
93
+
94
+ ## Test Data Patterns
95
+
96
+ ### Factory Pattern
97
+
98
+ Create test data with sensible defaults:
99
+
100
+ ```ts
101
+ // factories/user.ts
102
+ import { faker } from '@faker-js/faker';
103
+
104
+ interface UserFactoryOptions {
105
+ email?: string;
106
+ role?: 'user' | 'admin';
107
+ verified?: boolean;
108
+ }
109
+
110
+ export const createTestUser = (overrides: UserFactoryOptions = {}): User => ({
111
+ id: faker.string.uuid(),
112
+ email: overrides.email ?? faker.internet.email(),
113
+ name: faker.person.fullName(),
114
+ role: overrides.role ?? 'user',
115
+ verified: overrides.verified ?? true,
116
+ createdAt: faker.date.past(),
117
+ updatedAt: new Date(),
118
+ });
119
+
120
+ // Usage - only specify what matters for the test
121
+ const admin = createTestUser({ role: 'admin' });
122
+ const unverified = createTestUser({ verified: false });
123
+ ```
124
+
125
+ ### Builder Pattern
126
+
127
+ For complex objects with many configurations:
128
+
129
+ ```ts
130
+ // builders/OrderBuilder.ts
131
+ export class OrderBuilder {
132
+ private order: Partial<Order> = {};
133
+
134
+ withId(id: string): this {
135
+ this.order.id = id;
136
+ return this;
137
+ }
138
+
139
+ withItems(items: OrderItem[]): this {
140
+ this.order.items = items;
141
+ return this;
142
+ }
143
+
144
+ withStatus(status: OrderStatus): this {
145
+ this.order.status = status;
146
+ return this;
147
+ }
148
+
149
+ withDiscount(discount: Discount): this {
150
+ this.order.discount = discount;
151
+ return this;
152
+ }
153
+
154
+ shipped(): this {
155
+ this.order.status = 'shipped';
156
+ this.order.shippedAt = new Date();
157
+ return this;
158
+ }
159
+
160
+ build(): Order {
161
+ return {
162
+ id: this.order.id ?? faker.string.uuid(),
163
+ items: this.order.items ?? [],
164
+ status: this.order.status ?? 'pending',
165
+ discount: this.order.discount,
166
+ createdAt: new Date(),
167
+ updatedAt: new Date(),
168
+ } as Order;
169
+ }
170
+ }
171
+
172
+ // Usage
173
+ const order = new OrderBuilder()
174
+ .withItems([itemFactory()])
175
+ .withDiscount({ type: 'fixed', value: 10 })
176
+ .shipped()
177
+ .build();
178
+ ```
179
+
180
+ ### Object Mother Pattern
181
+
182
+ Pre-configured objects for common scenarios:
183
+
184
+ ```ts
185
+ // test/mothers/userMother.ts
186
+ export const UserMother = {
187
+ admin: () => createTestUser({ role: 'admin', verified: true }),
188
+ unverified: () => createTestUser({ verified: false }),
189
+ suspended: () => createTestUser({ status: 'suspended' }),
190
+ withOrders: async () => {
191
+ const user = await db.user.create({ data: createTestUser() });
192
+ await db.order.createMany({
193
+ data: [
194
+ createTestOrder({ userId: user.id }),
195
+ createTestOrder({ userId: user.id }),
196
+ ],
197
+ });
198
+ return user;
199
+ },
200
+ };
201
+
202
+ // Usage
203
+ const admin = UserMother.admin();
204
+ const userWithOrders = await UserMother.withOrders();
205
+ ```
206
+
207
+ ## Assertion Patterns
208
+
209
+ ### Focused Assertions
210
+
211
+ One logical assertion per test:
212
+
213
+ ```ts
214
+ // Bad: Multiple unrelated assertions
215
+ it('processes order', async () => {
216
+ const order = await processOrder(orderData);
217
+ expect(order.id).toBeDefined();
218
+ expect(order.status).toBe('confirmed');
219
+ expect(order.total).toBe(100);
220
+ expect(order.items).toHaveLength(2);
221
+ expect(sendEmail).toHaveBeenCalled();
222
+ expect(decrementInventory).toHaveBeenCalled();
223
+ });
224
+
225
+ // Good: Separate tests for each behavior
226
+ describe('processOrder', () => {
227
+ it('creates order with confirmed status', async () => {
228
+ const order = await processOrder(orderData);
229
+ expect(order.status).toBe('confirmed');
230
+ });
231
+
232
+ it('calculates correct total', async () => {
233
+ const order = await processOrder(orderData);
234
+ expect(order.total).toBe(100);
235
+ });
236
+
237
+ it('sends confirmation email', async () => {
238
+ await processOrder(orderData);
239
+ expect(sendEmail).toHaveBeenCalledWith(
240
+ expect.objectContaining({ type: 'order-confirmation' })
241
+ );
242
+ });
243
+ });
244
+ ```
245
+
246
+ ### Custom Matchers
247
+
248
+ Create domain-specific assertions:
249
+
250
+ ```ts
251
+ // test/matchers.ts
252
+ import { expect } from 'vitest';
253
+
254
+ expect.extend({
255
+ toBeValidEmail(received: string) {
256
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
257
+ const pass = emailRegex.test(received);
258
+ return {
259
+ pass,
260
+ message: () => `Expected ${received} ${pass ? 'not ' : ''}to be a valid email`,
261
+ };
262
+ },
263
+
264
+ toBeWithinRange(received: number, floor: number, ceiling: number) {
265
+ const pass = received >= floor && received <= ceiling;
266
+ return {
267
+ pass,
268
+ message: () =>
269
+ `Expected ${received} ${pass ? 'not ' : ''}to be within [${floor}, ${ceiling}]`,
270
+ };
271
+ },
272
+
273
+ toHaveBeenCalledWithUser(
274
+ received: jest.Mock,
275
+ expectedUser: Partial<User>
276
+ ) {
277
+ const calls = received.mock.calls;
278
+ const pass = calls.some(([user]) =>
279
+ Object.entries(expectedUser).every(([key, value]) => user[key] === value)
280
+ );
281
+ return {
282
+ pass,
283
+ message: () =>
284
+ `Expected mock ${pass ? 'not ' : ''}to have been called with user matching ${JSON.stringify(expectedUser)}`,
285
+ };
286
+ },
287
+ });
288
+
289
+ // Usage
290
+ expect(user.email).toBeValidEmail();
291
+ expect(score).toBeWithinRange(0, 100);
292
+ expect(mockSendEmail).toHaveBeenCalledWithUser({ role: 'admin' });
293
+ ```
294
+
295
+ ### Snapshot Testing
296
+
297
+ Use sparingly for complex, stable outputs:
298
+
299
+ ```ts
300
+ // Good use: Serialized output that rarely changes
301
+ it('generates correct API response format', () => {
302
+ const response = formatApiResponse(data);
303
+ expect(response).toMatchSnapshot();
304
+ });
305
+
306
+ // Bad use: Dynamic content that changes frequently
307
+ it('renders user profile', () => {
308
+ const profile = render(<UserProfile user={user} />);
309
+ expect(profile).toMatchSnapshot(); // Will break on any change
310
+ });
311
+
312
+ // Better: Inline snapshots for small outputs
313
+ it('formats currency correctly', () => {
314
+ expect(formatCurrency(1234.56, 'USD')).toMatchInlineSnapshot(`"$1,234.56"`);
315
+ });
316
+ ```
317
+
318
+ ## Test Organization Patterns
319
+
320
+ ### Describe Blocks by Behavior
321
+
322
+ ```ts
323
+ describe('OrderService', () => {
324
+ describe('createOrder', () => {
325
+ describe('with valid input', () => {
326
+ it('creates order record');
327
+ it('decrements inventory');
328
+ it('sends confirmation email');
329
+ });
330
+
331
+ describe('with invalid input', () => {
332
+ it('rejects empty cart');
333
+ it('rejects out-of-stock items');
334
+ it('rejects invalid user');
335
+ });
336
+ });
337
+
338
+ describe('cancelOrder', () => {
339
+ describe('when order is pending', () => {
340
+ it('updates status to cancelled');
341
+ it('restores inventory');
342
+ it('refunds payment');
343
+ });
344
+
345
+ describe('when order is shipped', () => {
346
+ it('throws CannotCancelError');
347
+ });
348
+ });
349
+ });
350
+ ```
351
+
352
+ ### Setup Hierarchy
353
+
354
+ ```ts
355
+ describe('API Routes', () => {
356
+ // Runs once before all tests in this describe
357
+ beforeAll(async () => {
358
+ await db.$connect();
359
+ });
360
+
361
+ afterAll(async () => {
362
+ await db.$disconnect();
363
+ });
364
+
365
+ describe('/users', () => {
366
+ // Runs before each test in /users
367
+ beforeEach(async () => {
368
+ await db.user.deleteMany();
369
+ });
370
+
371
+ describe('GET /users', () => {
372
+ it('returns empty list when no users');
373
+ it('returns all users');
374
+ });
375
+
376
+ describe('POST /users', () => {
377
+ it('creates user with valid data');
378
+ it('rejects invalid email');
379
+ });
380
+ });
381
+
382
+ describe('/orders', () => {
383
+ // Different setup for orders tests
384
+ beforeEach(async () => {
385
+ await db.order.deleteMany();
386
+ await db.user.deleteMany();
387
+ // Orders tests need a user
388
+ await db.user.create({ data: testUser });
389
+ });
390
+ });
391
+ });
392
+ ```
393
+
394
+ ## Anti-Patterns to Avoid
395
+
396
+ ### Testing Implementation Details
397
+
398
+ ```ts
399
+ // Bad: Couples test to implementation
400
+ it('uses lodash sortBy internally', () => {
401
+ const spy = vi.spyOn(_, 'sortBy');
402
+ sortUsers(users);
403
+ expect(spy).toHaveBeenCalled();
404
+ });
405
+
406
+ // Good: Tests behavior
407
+ it('sorts users by name ascending', () => {
408
+ const result = sortUsers([
409
+ { name: 'Charlie' },
410
+ { name: 'Alice' },
411
+ { name: 'Bob' },
412
+ ]);
413
+ expect(result.map(u => u.name)).toEqual(['Alice', 'Bob', 'Charlie']);
414
+ });
415
+ ```
416
+
417
+ ### Excessive Mocking
418
+
419
+ ```ts
420
+ // Bad: Everything is mocked
421
+ const mockDb = vi.fn();
422
+ const mockCache = vi.fn();
423
+ const mockLogger = vi.fn();
424
+ const mockConfig = vi.fn();
425
+
426
+ it('does something', () => {
427
+ // What are we even testing?
428
+ });
429
+
430
+ // Good: Integration test with real dependencies
431
+ it('persists user to database', async () => {
432
+ const user = await userService.create(userData);
433
+ const saved = await db.user.findUnique({ where: { id: user.id } });
434
+ expect(saved.email).toBe(userData.email);
435
+ });
436
+ ```
437
+
438
+ ### Test Interdependence
439
+
440
+ ```ts
441
+ // Bad: Tests depend on each other
442
+ let createdId: string;
443
+
444
+ it('creates item', async () => {
445
+ const item = await create({ name: 'Test' });
446
+ createdId = item.id; // Stored for next test
447
+ });
448
+
449
+ it('gets item', async () => {
450
+ const item = await get(createdId); // Depends on previous test
451
+ expect(item.name).toBe('Test');
452
+ });
453
+
454
+ // Good: Each test is self-contained
455
+ it('creates and retrieves item', async () => {
456
+ const created = await create({ name: 'Test' });
457
+ const retrieved = await get(created.id);
458
+ expect(retrieved.name).toBe('Test');
459
+ });
460
+ ```
461
+
462
+ ### Magic Values
463
+
464
+ ```ts
465
+ // Bad: What does 42 mean?
466
+ it('calculates something', () => {
467
+ expect(calculate(42)).toBe(84);
468
+ });
469
+
470
+ // Good: Named constants explain intent
471
+ it('doubles the input value', () => {
472
+ const input = 42;
473
+ const expected = input * 2;
474
+ expect(double(input)).toBe(expected);
475
+ });
476
+
477
+ // Or with descriptive test name
478
+ it('returns double the input', () => {
479
+ expect(double(5)).toBe(10);
480
+ });
481
+ ```
482
+
483
+ ## Naming Conventions
484
+
485
+ ### Test Names Should Be Sentences
486
+
487
+ ```ts
488
+ // Bad: Vague names
489
+ it('works');
490
+ it('handles error');
491
+ it('test 1');
492
+
493
+ // Good: Descriptive sentences
494
+ it('returns empty array when no users exist');
495
+ it('throws NotFoundError when user does not exist');
496
+ it('sends welcome email after user registration');
497
+ ```
498
+
499
+ ### Use Consistent Prefixes
500
+
501
+ ```ts
502
+ // Action verbs
503
+ it('creates order with valid data');
504
+ it('updates user profile');
505
+ it('deletes expired sessions');
506
+
507
+ // Condition verbs
508
+ it('returns null when user not found');
509
+ it('throws ValidationError for invalid email');
510
+ it('rejects duplicate email addresses');
511
+ ```